一起撸个朋友圈吧 - ListView(上)篇

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

这个项目将会一个系列,但在下不敢保证我的更新速度。。。

不过在下更新后会在简书首发,欢迎订阅和fork项目

简书文集:http://www.jianshu.com/notebooks/3224048/latest

本文地址:http://www.jianshu.com/p/7fa237cfddbb

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

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

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

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

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

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

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

话不多说,预览图地址(GIF图,5.6M,过大无法直接上传到论坛,所以只好委屈一下了):

https://github.com/razerdp/FriendCircle/blob/master/img/2016-02-10%20%E4%B8%8B%E6%8B%89%E5%88%B7%E6%96%B0.gif

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

因为不制造重复的轮子这个名言,同时根据这篇文章(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的一部分,那么我暂时想到以下两个方案:

  • icon使用imageview,在布局文件中单独存在而不是作为listview的一部分

  • icon使用imageview,使用WindowManager动态添加一个

为了方便(偷懒),我采用了第一个方案。于是我们的布局文件就出来了:
我知道直接复制xml代码是又长又臭的,所以在下截了个图:

1.png

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

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

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

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

  • 先画出listview

  • 再画出我们的icon(让其在Listviews上方)

  • 最后画出actionbar(让其可以盖住icon和listview)

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

  • 在listview下拉的时候,将距离回调中控制我们的icon距离顶部的距离(topMargin),同时listview也下拉,两者互不干扰

  • 当拉到了刷新距离的时候,松手,listview回弹,icon因为设置了margin,所以会保持刷新距离那个位置,此时播放动画(不断地旋转),同时执行刷新操作

  • 在刷新完成后,因为我们的listview已经回弹,此时没有任何位移信息可以使用,所以我们需要用一个线程来手动做一个插值器,动态更新icon的margin,使之回到最顶部隐藏在actionbar下方。

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

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

2.png

我们初步定义高度为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。


16/02/15:

关于recylerview和listview,详见评论区。

非常感谢评论区的兄弟建议,的确,在下也认为用valueAnimator更为合适,毕竟不用添加一个工具类对吧。。

在git已经更新代码,更新如下:

  /**刷新完成*/
        @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();
        }

非常感谢-V-也希望大家有更好的方案可以提出来,当然,也十分欢迎在git PR