一塊兒擼個朋友圈吧(step1) - ListView(上)篇

項目地址:github.com/razerdp/Fri…java

下篇連接:http://www.jianshu.com/p/94e1e267b3b3android


題目命名是否是很簡單粗暴←_←ios

咳咳,進入正題,關於本項目什麼的,在GitHub都寫得清清楚楚了,咱們就不廢話,直接進入主題。git

微信朋友圈在我認識的版本中,有兩個(廢話orz),一個是IOS,一個是Android,(再次廢話)。github

其中IOS由於得天獨厚的UI實現優點,能夠輕鬆地作出各類看起來順眼並且又頗有逼格的動畫,這可苦了Android了,相較之下,Android爲了實現幾個動畫就必須得多寫N行代碼,就好比朋友圈的下拉刷新。微信

朋友圈的下拉刷新在兩個系統裏有一個很明顯的區別,在於刷新的那個icon,在android中,刷新的Icon永遠都處於headerview中,並且是在headerview的底部,沒法突破headerview的限制,而在ios版本中,icon不受listview控制,這二者彷佛是分離的。所以在ios中,刷新的icon是能夠隨着listview的下拉而被一塊兒拉下來。框架

上文提及來也許有點不清楚,你們能夠找找兩個系統的手機一塊兒刷一次,留意一下刷新Icon的動做,就知道怎麼回事了。ide

那麼做爲一枚高逼格(苦逼)的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的一部分,那麼我暫時想到如下兩個方案:

  • icon使用imageview,在佈局文件中單獨存在而不是做爲listview的一部分
  • icon使用imageview,使用WindowManager動態添加一個

爲了方便(偷懶),我採用了第一個方案。因而咱們的佈局文件就出來了: 我知道直接複製xml代碼是又長又臭的,因此在下截了個圖:

主佈局

能夠看到,咱們的佈局十分簡潔,從上到下是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那一部分的顏色,因此它的佈局也是十分的簡單:

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();
        }
複製代碼

兩個方法都保留了

相關文章
相關標籤/搜索