专业IM即时通讯软件开发,值得信赖!

Android手把手朋友圈实战教程(一)ListView(上)

即时通讯软件开发 云聊IM 843℃

项目地址:https://github.com/razerdp/FriendCircle

咳咳,进入正题,关于本项目什么的,在GitHub都写得清清楚楚了,我们就不废话,直接进入主题。

微信朋友圈在我认识的版本中,有两个(废话orz),一个是IOS,一个是Android,(再次废话)。

其中IOS因为得天独厚的UI实现优势,可以轻松地做出各种看起来顺眼而且又很有逼格的动画,这可苦了Android了,相较之下,Android为了实现几个动画就必须得多写N行代码,就比如朋友圈的下拉刷新。

朋友圈的下拉刷新在两个系统里有一个很明显的区别,在于刷新的那个icon,在android中,刷新的Icon永远都处于headerview中,而且是在headerview的底部,无法突破headerview的限制,而在ios版本中,icon不受listview控制,这两者似乎是分离的。因此在ios中,刷新的icon是可以随着listview的下拉而被一起拉下来。

上文说起来也许有点不清楚,大家可以找找两个系统的手机一起刷一次,留意一下刷新Icon的动作,就知道怎么回事了。

那么作为一枚高逼格(苦逼)的android程序猿,我们当然要挑战ios的刷新啦是不是。

于是,就有了我们的这个系列的第一篇(说好的不废话呢)

话不多说,预览图送上:(请忽略穹妹)

开工之前,我们先分析一下实现的方案

因为不制造重复的轮子这个名言,同时根据这篇文章(https://github.com/desmond1121/Android-Ptr-Comparison ) 的分析,我就选用了android-Ultra-Pull-To-Refresh这个库来进行扩展。

(库git:https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh

这个库的优点在于其强大的扩展性和可定制性,所以选它无可厚非。

库选择完毕,接下来就是思考了。

首先,我们的刷新icon要突破listview限制,那么这个icon绝对不可以是listview的一部分,那么我暂时想到以下两个方案:

  1. icon使用imageview,在布局文件中单独存在而不是作为listview的一部分
  2. icon使用imageview,使用WindowManager动态添加一个

为了方便(偷懒),我采用了第一个方案。于是我们的布局文件就出来了:

我知道直接复制xml代码是又长又臭的,所以在下截了个图:

主布局

可以看到,我们的布局十分简洁,从上到下是listview->imageview->actionbar,为什么我要这么放呢,这就关乎到布局文件的绘制顺序问题了,

绘制(Drawing)是从布局的根结点开始的,布局层次的绘制顺序为声明的顺序,例如,父view的绘制先于它的子view,而子view的绘制顺序也是按照声明的顺序。

简单的说,在视觉上,就是先画上面的,再画下面的。

所以我们的布局就这么写:

  1. 先画出listview
  2. 再画出我们的icon(让其在Listviews上方)
  3. 最后画出actionbar(让其可以盖住icon和listview)

写到这里,我们大概就知道实现的方案:

  1. 在listview下拉的时候,将距离回调中控制我们的icon距离顶部的距离(topMargin),同时listview也下拉,两者互不干扰
  2. 当拉到了刷新距离的时候,松手,listview回弹,icon因为设置了margin,所以会保持刷新距离那个位置,此时播放动画(不断地旋转),同时执行刷新操作
  3. 在刷新完成后,因为我们的listview已经回弹,此时没有任何位移信息可以使用,所以我们需要用一个线程来手动做一个插值器,动态更新icon的margin,使之回到最顶部隐藏在actionbar下方。

上面的方案看起来很复杂,事实上也确实有点复杂,但幸运的是,下拉框架已经实现了最麻烦的接口,得益于PtrUIHandler和PtrHandler这两个回调,我们起码节省了70%的时间。

接下来我们先初步实现header。

我们的header没啥功能,它只有一个作用,就是下拉后的overscroll那一部分的颜色,所以它的布局也是十分的简单:

header布局

我们初步定义高度为300dp,因为在我的测试中,即使我从顶部拉到底部,我们的header还是没有显示完(得益于阻尼参数),所以300dp足够了

布局完成后,我们撸出我们的代码:

public class FriendCirclePtrHeader extends RelativeLayout {
    private static final String TAG = "FriendCirclePtrHeader";

    private ImageView mRotateIcon;
    private View rootView;
    private boolean isAutoRefresh;
    private RotateAnimation rotateAnimation;
    private SmoothChangeThread mSmoothChangeThread;

    //当前状态
    private PullStatus mPullStatus;

    public FriendCirclePtrHeader(Context context) {
        this(context, null);
    }

    public FriendCirclePtrHeader(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FriendCirclePtrHeader(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public FriendCirclePtrHeader(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView(context);
    }

    private void initView(Context context) {
        rootView = LayoutInflater.from(context).inflate(R.layout.widget_ptr_header, this, false);
        addView(rootView);

        rotateAnimation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
                0.5f);
        rotateAnimation.setDuration(600);
        rotateAnimation.setInterpolator(new LinearInterpolator());
        rotateAnimation.setRepeatCount(Animation.INFINITE);
    }
}

我们直接inflate一个view出来,然后添加到我们的header中,同时初始化一些anima

接下来就是最主要的实现部分:

//=============================================================ptr:
private PtrUIHandler mPtrUIHandler = new PtrUIHandler() {
    /**回到初始位置*/
    @Override
    public void onUIReset(PtrFrameLayout frame) {
        mPullStatus = PullStatus.NORMAL;
        if (mRotateIcon.getAnimation() != null) {
            mRotateIcon.clearAnimation();
        }
    }

    /**离开初始位置*/
    @Override
    public void onUIRefreshPrepare(PtrFrameLayout frame) {

    }

    /**开始刷新动画*/
    @Override
    public void onUIRefreshBegin(PtrFrameLayout frame) {
        mPullStatus = PullStatus.REFRESHING;
        if (mRotateIcon != null) {
            if (mRotateIcon.getAnimation() != null) {
                mRotateIcon.clearAnimation();
            }
            mRotateIcon.startAnimation(rotateAnimation);
        }
    }

    /**刷新完成*/
    @Override
    public void onUIRefreshComplete(PtrFrameLayout frame) {
        mPullStatus = PullStatus.NORMAL;
        if (mSmoothChangeThread==null){
            mSmoothChangeThread=SmoothChangeThread.CreateLinearInterpolator(mRotateIcon,frame.getOffsetToRefresh
                    (),0,300,75);
            mSmoothChangeThread.setOnSmoothResultChangeListener(new SmoothChangeThread.OnSmoothResultChangeListener() {
                @Override
                public void onSmoothResultChange(int result) {
                    updateRotateAnima(result);
                    mRotateIcon.setRotation(-(result << 1));
                }
            });
        }else {
            mSmoothChangeThread.stop();
        }
        mRotateIcon.post(mSmoothChangeThread);

    }

    /**位移更新重载*/
    @Override
    public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) {
        final int mOffsetToRefresh = frame.getOffsetToRefresh();
        final int currentPos = ptrIndicator.getCurrentPosY();
        final int lastPos = ptrIndicator.getLastPosY();

        if (currentPos < mOffsetToRefresh) {
            //未到达刷新线
            if (status == PtrFrameLayout.PTR_STATUS_PREPARE && mRotateIcon != null) {
                updateRotateAnima(currentPos);
                mRotateIcon.setRotation(-(currentPos << 1));
            }
        }
        else if (currentPos > mOffsetToRefresh) {
            //到达或超过刷新线
            if (isUnderTouch && status == PtrFrameLayout.PTR_STATUS_PREPARE && mRotateIcon != null) {
                updateRotateAnima(mOffsetToRefresh);
                mRotateIcon.setRotation(-(currentPos << 1));
            }
        }
    }
};

private void updateRotateAnima(int marginTop) {
    Log.d(TAG, "curMargin=========" + marginTop);
    if (mRotateIcon == null) return;
    RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mRotateIcon.getLayoutParams();
    params.topMargin = marginTop;
    mRotateIcon.setLayoutParams(params);
}

ptruihandler是框架暴露给我们用来控制UI下拉时的回调,相关信息都已经在注释中写明了。

这里我们主要关注这个回调:

onUIRefreshComplete

这个回调是当刷新完成后,外部执行ptrframe.refreshComplete()时会执行,但我们的listview已经回弹了,也就是说没有任何位移信息供我们更新topMargin,如果没有位移,我们直接 updateRotateAnima(0)的话,在画面上展示出来的就是我们的icon一下子就消失了,而没有一个过渡的动画,因此我们通过一个线程来执行这个动作

/**
 * @desc 平滑滚动线程,用于递归调用自己来实现某个视图的平滑滚动
 * */
public class SmoothChangeThread implements Runnable {
    //需要操控的视图
    private View v = null;
    //原Y坐标
    private int fromY = 0;
    //目标Y坐标
    private int toY = 0;
    //动画执行时间(毫秒)
    private long durtion = 0;
    //帧率
    private int fps = 60;
    //间隔时间(毫秒),间隔时间 = 1000 / 帧率
    private int interval = 0;
    //启动时间,-1 表示尚未启动
    private long startTime = -1;
    //减速插值器
    private static Interpolator mInterpolator = null;
    private OnSmoothResultChangeListener mListener;

    public static SmoothChangeThread CreateLinearInterpolator(View v, int fromY, int toY, long durtion, int fps){
        mInterpolator=new LinearInterpolator();
        return new SmoothChangeThread(v,fromY,toY,durtion,fps);
    }
    public static SmoothChangeThread CreateDecelerateInterpolator(View v, int fromY, int toY, long durtion, int fps){
        mInterpolator=new DecelerateInterpolator();
        return new SmoothChangeThread(v,fromY,toY,durtion,fps);
    }
    public static SmoothChangeThread CreateAccelerateDecelerateInterpolator(View v, int fromY, int toY, long durtion, int fps){
        mInterpolator=new AccelerateDecelerateInterpolator();
        return new SmoothChangeThread(v,fromY,toY,durtion,fps);
    }

    /**
     *
     * @param v view
     * @param fromY 原始数据
     * @param toY 目标数据
     * @param durtion 持续时间
     * @param fps 帧数
     */
    private SmoothChangeThread(View v, int fromY, int toY, long durtion, int fps) {
        this.v = v;
        this.fromY = fromY;
        this.toY = toY;
        this.durtion = durtion;
        this.fps = fps;
        this.interval = 1000 / this.fps;
    }

    @Override
    public void run() {
        //先判断是否是第一次启动,是第一次启动就记录下启动的时间戳,该值仅此一次赋值
        if (startTime == -1) {
            startTime = System.currentTimeMillis();
        }
        //得到当前这个瞬间的时间戳
        long currentTime = System.currentTimeMillis();
        //放大倍数,为了扩大除法计算的浮点精度
        int enlargement = 1000;
        //算出当前这个瞬间运行到整个动画时间的百分之多少
        float rate = (currentTime - startTime) * enlargement / durtion;
        //这个比率不可能在 0 - 1 之间,放大了之后即是 0 - 1000 之间
        rate = Math.max(Math.min(rate, 1000),0);
        //将动画的进度通过插值器得出响应的比率,乘以起始与目标坐标得出当前这个瞬间,视图应该滚动的距离。
        int changeDistance = Math.round((fromY - toY) * mInterpolator.getInterpolation(rate / enlargement));
        int currentY = fromY - changeDistance;
        if (mListener!=null){
            mListener.onSmoothResultChange(currentY);
        }

        if (currentY != toY) {
            v.postDelayed(this, this.interval);
        }
        else {
            return;
        }
    }

    public void stop() {
        v.removeCallbacks(this);
        startTime=-1;
    }

    public OnSmoothResultChangeListener getOnSmoothResultChangeListener() {
        return mListener;
    }

    public void setOnSmoothResultChangeListener(OnSmoothResultChangeListener listener) {
        mListener = listener;
    }

    public interface OnSmoothResultChangeListener{
        void onSmoothResultChange(int result);
    }
}

这个java源文件是在网上找的自定义插值器,我经过修改后,通过接口回调把计算结果抛出去,并且使用静态工厂提供不同类型的插值器效果,我们就可以通过这个接口来动态更新我们的margin了(ps:这个工具类还可以用在很多地方呢)

文章至此,我们的header基本定制完成,完整代码可以查看github,下一步要实现的就是对ptrframe的封装,让其变成我们的ptrlistview。

=====================

这几天收到了一些评论,大致如下:

  1. 为何不用recylerview
  2. 为何不用valueanimator代替线程

现在回答如下:

  1. 因为目前说实话,大多数项目一直都是用着listview,而且牵扯比较深了,所以这里就用listview,其次,其实在下很喜欢recylerview的说。。。。。另外,框架支持添加任意view,所以喜欢的话可以换成recylerview。
  2. 当时拼命想着如何去更新这个margin,于是脑里面蹦出了一个“线程计算啊笨蛋”,于是就干了。看了评论才忽然发现。。。。为何我不用valueanimator啊,我笨啊!!!现在在git更新了。两种方法-V-

更新代码如下:

/**刷新完成*/
@Override
public void onUIRefreshComplete(PtrFrameLayout frame) {
    mPullState = PullState.NORMAL;
    if (mRotateIcon==null)return;
    /**采取通用插值器线程实现*/
   /* if (mSmoothChangeThread == null) {
        mSmoothChangeThread = SmoothChangeThread.CreateLinearInterpolator(mRotateIcon,
                frame.getOffsetToRefresh(), 0, 300, 75);
        mSmoothChangeThread.setOnSmoothResultChangeListener(
                new SmoothChangeThread.OnSmoothResultChangeListener() {
                    @Override
                    public void onSmoothResultChange(int result) {
                        updateRotateAnima(result);
                        mRotateIcon.setRotation(-(result << 1));
                    }
                });
    }
    else {
        mSmoothChangeThread.stop();
    }
    mRotateIcon.post(mSmoothChangeThread);*/

    /**采取valueAnimator*/
    if (mValueAnimator==null){
        mValueAnimator=ValueAnimator.ofInt(frame.getOffsetToRefresh(),0);
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int result= (int) animation.getAnimatedValue();
                updateRotateAnima(result);
                mRotateIcon.setRotation(-(result << 1));
            }
        });
        mValueAnimator.setDuration(300);
    }
    mValueAnimator.start();
}

原文链接:https://www.jianshu.com/p/7fa237cfddbb

版权声明:部分文章、图片等内容为用户发布或互联网整理而来,仅供学习参考。如有侵犯您的版权,请联系我们,将立刻删除。
喜欢 (0)
仿微信聊天软件开发
点击这里给我发消息