第一站小紅書圖片裁剪控件之二,自定義CoordinatorLayout聯動效果

本篇續:java

第一站小紅書圖片裁剪控件,深度解析大廠炫酷控件android

先來看看幾張效果圖: git

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
emmmm,想感覺高清絲滑的動畫效果,有如下兩種方式:

https://github.com/HpWens/MeiWidgetView 歡迎Star

https://www.pgyer.com/zKF4 APK地址

在前篇中已經講了相關手勢的處理,本篇重點講解留白,列表聯動效果。程序員

在上一篇中因爲篇幅緣由,圖片左下角裁剪狀態的切換並無講解,經過分析小紅書,有如下4種狀態: github

圖1
圖2
在這裏插入圖片描述
在這裏插入圖片描述
分別對應:裁切,填滿,留白,充滿。這裏的裁切,填滿(是樓主大大取的中文名字,不必定準確),他們分別對應圖1,圖2。那麼4種狀態怎麼控制圖片的顯示?

  • 裁切,改變圖片的顯示區域,在前文中已經提到圖片有任意尺寸,默認顯示的區域爲寬高相等的矩形區域(正方形區域),而在裁切狀態下,顯示的區域爲寬高不相等的區域。以最小邊爲基準,剩餘的一邊縮至原來的四分之三,那麼什麼又是基準呢?這裏以簡單的公式來理解:
圖片寬度 = a 
 圖片高度 = b
複製代碼

若是 a > b 則以寬度爲基準,反之以高度有基準。以==demo==中的圖片爲例: 緩存

ic_gril.png
圖片分辨率爲 360*240 寬大於高的圖片,那麼以 寬度爲基準,控件高度縮放四分之三,最後裁切的效果以下:
在這裏插入圖片描述

  • 留白,在圖片四周有白邊,保證圖片一邊鋪滿控件,另外一邊出現白邊,白邊的區域大小與圖片的實際尺寸有關。app

  • 填滿,與充滿同爲默認狀態,鋪滿控件,顯示區域爲寬高相等的矩形區域(正方形區域)。ide

對了,這裏有一點須要說明,裁切狀態下控件一邊縮放至四分之三長度,與小紅書是有差別的,小紅書是根據圖片實際尺寸改變裁切區域,取的最小值纔是四分之三。工具

構思代碼

裁切

裁切,本質就是改變控件顯示區域,那麼怎麼改變控件顯示區域,你們必定會想到改變控件大小,對自定義view繪製流程熟悉的小夥伴確定會知道,在測量onMeasure方法中經過改變MeasureSpec.getSize()測量大小從而改變控件大小。但小編並不想改變控件大小,而是想改變控件的顯示區域,用官方說法,就是改變控件的佈局區域。測量 - 佈局 - 繪製,自定義view的三步驟,佈局相關方法以下:佈局

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }
    // onLayout layout 差別省略,這裏重寫layout方法
    @Override
    public void layout(int l, int t, int r, int b) {
        super.layout(l, t, r, b);
    }
複製代碼

咱們能夠改變 super.layout(l, t, r, b);l, t, r, b 的值來控制控件的顯示區域。注意這裏 r, b 含義:

r = l + 控件寬度 
 b = t + 控件高度
複製代碼

填滿、充滿

填滿、充滿同默認狀態。

留白

一邊鋪滿,一邊留白邊,白邊的區域大小跟圖片尺寸有關,圖片尺寸比例越接近1.0白邊越小,反之越大。記得,在前篇中爲了保證圖片鋪滿控件,縮放取值以下:

Math.max( 控件寬度/圖片寬度 , 控件高度/圖片高度 )
複製代碼

那麼只保證一邊鋪滿,只須要取最小值就能夠了:

Math.min( 控件寬度/圖片寬度 , 控件高度/圖片高度 )
複製代碼

編寫代碼

裁切

裁切分爲如下兩步:

  1. 斷定寬或高爲基準邊:
// 獲取圖片的寬度和高度
   Drawable drawable = getDrawable();
   if (null == drawable) {
       return;
   }
   int drawableWidth = drawable.getIntrinsicWidth();
   int drawableHeight = drawable.getIntrinsicHeight();
   // mIsWidthLarger true 寬度有基準邊 高度裁剪 false 高度爲基準邊 寬度裁剪
   mIsWidthLarger = drawableWidth > drawableHeight;
複製代碼
  1. 重寫layout方法,改變顯示寬高:
@Override
    public void layout(int l, int t, int r, int b) {
        if (mIsCrop && l == 0 && t == 0) {
            float scaleRatio = 1.0F;
            float defaultRatio = 1.0F;

            if (mIsWidthLarger) {
                // 高度爲原高度 3/4 居中
                scaleRatio = defaultRatio + defaultRatio / 4F;
            } else {
                // 寬度爲原寬度 3/4 居中
                scaleRatio = defaultRatio - defaultRatio / 4F;
            }

            int width = r - l;
            int height = b - t;

            if (scaleRatio > defaultRatio) {
                int offsetY = (int) (height * (scaleRatio - defaultRatio) / 2F);
                // 除了2 上加下減 改變高度顯示區域
                t += offsetY;
                b -= offsetY;
            } else if (scaleRatio < defaultRatio) {
                int offsetX = (int) (width * (defaultRatio - scaleRatio) / 2F);
                // 左加右減 改變寬度顯示區域
                l += offsetX;
                r -= offsetX;
            }
        }
        super.layout(l, t, r, b);
    }
複製代碼

有不明白的地方,請參考註釋或留言,效果圖就像這樣:

在這裏插入圖片描述

留白

填滿、充滿爲默認狀態,在前篇已經講解過了。留白,一邊留白一邊鋪滿,那麼圖片的縮放比例就會發生改變,還記得前篇中的縮放比例嗎:

Math.max(控件寬度/圖片寬度,控件高度/圖片高度)
複製代碼

這樣就能保證圖片最小邊鋪滿控件,留白效果偏偏相反,圖片最小邊不須要鋪滿控件(兩邊留白,居中對齊),同時還須要保證非最小邊鋪滿控件,那麼圖片縮放比例應該取最小值,就像這樣:

@Override
    public void onGlobalLayout() {
            // 省略...... 
            // 圖片縮放比
            mBaseScale = mIsLeaveBlank ? Math.min((float) viewWidth / drawableWidth, (float) viewHeight / drawableHeight) : Math.max((float) viewWidth / drawableWidth, (float) viewHeight / drawableHeight);
  
    }
複製代碼

mIsLeaveBlank 參數控制是否留白,true 取最小值;false 取最大值。

留白改變了圖片顯示區域,那麼==邊界檢測== 的越界斷定條件也會發生變化,讓咱們一塊兒來回憶一下,非留白越界斷定條件:

// 邊界檢測
    private void boundCheck() {
        // 獲取圖片矩陣
        RectF rectF = getMatrixRectF();
        if (rectF.left >= 0) {
            // 左越界
        }
        if (rectF.top >= 0) {
            // 上越界
        }
        if (rectF.right <= getWidth()) {
            // 右越界
        }
        if (rectF.bottom <= getHeight()) {
            // 下越界
        }
    }
複製代碼

那麼留白的越界斷定條件又是什麼呢?先來看張圖,注意左右留白的紅色實線:

在這裏插入圖片描述
如上圖,留白的狀況下,左右越界的條件就須要左加右減紅線部分,那麼紅線的長度又爲多少呢?

紅線長度 = (控件寬度 -  圖片寬度) / 2 
複製代碼

獲取到留白長度,左越界的條件就須要加上留白的長度:

RectF rectF = getMatrixRectF();
        
        float rectWidth = rectF.right - rectF.left;
        float rectHeight = rectF.bottom - rectF.top;

        // 獲取到左右留白的長度
        int leftLeaveBlankLength = (int) ((getWidth() - rectWidth) / 2);
        leftLeaveBlankLength = leftLeaveBlankLength <= 0 ? 0 : leftLeaveBlankLength;
        
        float leftBound = mIsLeaveBlank ? leftLeaveBlankLength : 0;
        if (rectF.left >= 0 + leftBound) {
            // 左越界
            startBoundAnimator(rectF.left, 0 + leftBound, true);
        }
複製代碼

右越界須要減去留白的長度:

float rightBound = mIsLeaveBlank ? getWidth() - leftLeaveBlankLength : getWidth();
        if (rectF.right <= rightBound) {
            // 右越界
            startBoundAnimator(rectF.left, rightBound - rectWidth, true);
        }
複製代碼

上下越界的狀況同左右越界的狀況,好了,來看下效果圖:

在這裏插入圖片描述

緩存,壓縮,保存裁剪圖片

緩存

有關LruCache的介紹,郭霖大神的 Android DiskLruCache徹底解析,硬盤緩存的最佳方案 這篇文章依舊記憶猶新。使用很是簡單:

// 圖片緩存
   private LruCache<String, Bitmap> mLruCache;
   // 根據實際狀況 設置 maxSize 大小
   mLruCache = new LruCache<>(Integer.MAX_VALUE);
   /** * @param path 圖片地址 */
   public synchronized void setImagePath(String path) {
       if (path != null && !path.equals("")) {
           Bitmap lruBitmap = mLruCache.get(path);
           if (lruBitmap == null) {
               // 圖片壓縮
               Bitmap bitmap = BitmapUtils.getCompressBitmap(getContext(), path);
               mLruCache.put(path, bitmap);
               lruBitmap = bitmap;
           }
           if (lruBitmap != null) {
               mFirstLayout = true;
               mMaxScale = 3.0F;
               // 根據實際狀況改變留白裁切狀態
               setImageBitmap(lruBitmap);
               onGlobalLayout();
           }
       }
   }
   
複製代碼

清除緩存:

@Override
    protected void onDetachedFromWindow() {
        // 清除緩存
        if (mLruCache != null) {
            mLruCache.evictAll();
        }
    }
複製代碼

壓縮

相信有關圖片的壓縮你們也是知根知底,這裏就簡單的貼下代碼:

public static Bitmap getCompressBitmap(Context context, String path) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        // 不加載到內存中
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path, options);
        // 斷定是不是橫豎圖
        boolean verEnable = options.outWidth < options.outHeight;
        int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
        int screenHeight = context.getResources().getDisplayMetrics().heightPixels;
        options.inSampleSize = BitmapUtils.calculateInSampleSize(options, verEnable ? screenWidth : screenHeight, verEnable ? screenHeight : screenWidth);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFile(path, options);
    }
複製代碼

裁剪圖片

最終咱們須要獲得,控件區域內的圖片並轉換成bitmap,咱們能夠借鑑如下方法:

/** * @param leaveBlankColor 留白區域顏色 * @return @return view轉換成bitmap */
    public Bitmap convertToBitmap(int leaveBlankColor) {
        int w = getWidth();
        int h = getHeight();
        Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bmp);
        c.drawColor(leaveBlankColor);
        layout(0, 0, w, h);
        draw(c);
        return bmp;
    }
複製代碼

在小紅書中若是再次切到選中的圖片,圖片處於上次操做狀態(記憶),說的簡明點,圖片的x,y軸平移以及縮放比同上次操做同樣,怎麼實現呢,須要保存與恢復圖片位置縮放比信息。

保存:

/** * 獲取到位置信息 * * @return float[2] = { x座標, y座標 } */
    public float[] getLocation() {
        float[] values = new float[9];
        mMatrix.getValues(values);
        return new float[]{values[Matrix.MTRANS_X], values[Matrix.MTRANS_Y]};
    }

    /** * @return 獲取圖片縮放比 */
    private float getScale() {
        float[] values = new float[9];
        mMatrix.getValues(values);
        return values[Matrix.MSCALE_X];
    }
複製代碼

恢復:

/** * 恢復位置信息 * * @param x 圖片平移x座標 * @param y 圖片平移y座標 * @param scale 圖片當前縮放比 */
    public void restoreLocation(float x, float y, float scale) {
        float[] values = new float[9];
        mMatrix.getValues(values);

        values[Matrix.MSCALE_X] = scale;
        values[Matrix.MSCALE_Y] = scale;

        values[Matrix.MTRANS_X] = x;
        values[Matrix.MTRANS_Y] = y;

        mMatrix.setValues(values);
        setImageMatrix(mMatrix);
    }
複製代碼

圖片裁剪控件還有一些細節,這裏就不一一講解了,有什麼疑問,歡迎留言討論?接下來重點講解列表聯動效果。

列表聯動

在這裏插入圖片描述
相信你們第一眼看到這個效果,必定會想到CoordinatorLayout的聯動效果,沒錯,小編剛開始也是想經過CoordinatorLayout去實現這個效果,而後就沒有而後了,不知道是否是本身的姿式不對,最終嗝屁了。 這個糟老頭壞得很,白白花費了我大量時間

最開始並無想到經過自定義view來實現相似CoordinatorLayout聯動效果,而是一頭扎進去研究CoordinatorLayout,查閱源碼,斷點分析,研究Behavior等,越走越遠,越想越複雜,本身離真理愈來愈遠。

心中一直有一個念頭,爲啥小紅書能夠實現,本身卻不行?是否是思路有問題?因而我再次用view層級分析工具,分析小紅書視圖層級:

在這裏插入圖片描述
當我看到這裏,內心那個暢快,原來小紅書也沒有使用CoordinatorLayout,而是用的LinearLayout線性佈局,若是是CoordinatorLayout這裏應該顯示ViewGroup,基本能夠確定小紅書是經過 自定義LinearLayout來實現列表聯動效果。

接下來拆分效果,同CoordinatorLayout聯動相似,一樣有展開與收起兩種狀態,支持 ==「甩」== filing 效果,在展開狀態下:

xml佈局層級:

<LinearLayout >
    
        <com.demo.mcropimageview.MCropImageView />
        
        <android.support.v7.widget.RecyclerView />

    </LinearLayout>
複製代碼
  1. 未觸碰MCropImageView區域,RecyclerView消費滑動事件,滾動列表

  2. 在RecyclerView區域向上滑動,觸碰到MCropImageView區域,RecyclerView與MCropImageView跟隨手指移動,向上滑動移出屏幕;向下滑動則移入屏幕,當MCropImageView徹底展現,MCropImageView中止移動,若是手指移動到RecyclerView區域,則消費滑動事件。

收起狀態:

  1. 未滑動到RecyclerView頂部,RecyclerView自身消費滑動事件

  2. 滑動到RecyclerView頂部並向下滑動,RecyclerView與MCropImageView跟隨手指移動,向下滑動移入屏幕,向上滑動移出屏幕,當MCropImageView徹底移出屏幕,繼續向上滑動,則RecyclerView消費滑動事件

大多數狀況下,當咱們要作一個View跟隨手指移動的效果時,都是直接setOnTouchListener或者直接重寫onTouchEvent去實現的,但這種方式用在咱們即將要作的這個效果上,就很不合適了,由於由於咱們是要作到能夠做用在任意一個View上的(這裏指RecyclerView與MCropImageView),這樣一來,若是目標View原本就已經重寫了OnTouchEvent或者設置了OnTouchListener,就極可能會滑動衝突,並且還很是不靈活,這個時候,使用自定義ViewGroup的方式是最佳選擇。上文中已經明確了使用自定義LinearLayout來實現列表的聯動效果。

構思代碼

聯動,聯動,那麼第一個問題就是解決, 的問題,怎樣讓view動起來?emmmm,這個難不倒我,動態改變view在父控件中的位置信息,在view中提供了一系列的方法來讓view動起來:

scrollBy,scrollTo,setTranslation,layout,offsetTopAndBottom,setScrollY等方法,效果圖上在手指擡起的時候,view會根據當前的滑動距離慣性滑動,那麼藉助OverScroller類實現慣性滑動就很是容易了。

知道了怎麼動,那麼動的距離呢,與RecyclerView滑動有關,重寫onTouchEvent獲取滑動偏移量,RecyclerView的父控件根據偏移量進行移動,在手指擡起時,根據偏移量斷定父控件是否展開,收起。

當手指鬆開,藉助VelocityTracker得到滑動速率,若是速率大於指定值,則斷定爲 「甩」,並經過Scroller來進行慣性移動,同時改變展開,收起狀態。

如手指鬆開後滑動速率低於指定值,則視爲 「放手」,這時候根據getScrollY是否大於指定值,並經過Scroller來進行展開或收起的慣性移動。

大概過程就是這樣,接下來開工寫代碼洛~

起名字

怎麼樣才能取一個接地氣的名字呢?我看就叫CoordinatorLinearLayout ,同時還須要自定義RecyclerView,咱們就叫它,CoordinatorRecyclerView。同時還給這兩個名字卜了一掛,哈哈,大吉還不錯。

編寫代碼

建立CoordinatorRecyclerView

好,那咱們來看看CoordinatorRecyclerView應該怎麼寫: 先是成員變量:

private int mTouchSlop = -1;
    private VelocityTracker mVelocityTracker;
    // 是否從新測量用於改變RecyclerView的高度
    private boolean mIsAgainMeasure = true;
    // 是否展開 默認爲true
    private boolean mIsExpand = true;
    // 父類最大的滾動區域 = 裁剪控件的高度
    private int mMaxParentScrollRange;
    // 父控件在y方向滾動的距離
    private int mCurrentParenScrollY = 0;
    // 最後RawY座標
    private float mLastRawY = 0;
    private float mDeltaRawY = 0;
    // 是否消費touch事件 true 消費RecyclerView接受不到滾動事件
    private boolean mIsConsumeTouchEvent = false;
    // 回調接口
    private OnCoordinatorListener mListener;
複製代碼

再到構造方法:

public CoordinatorRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // 用於處理手勢filing動做
        mVelocityTracker = VelocityTracker.obtain();
        // 最大滑動範圍 = 圖片裁剪控件高度 (圖片裁剪控件是寬高相等)
        mMaxParentScrollRange = context.getResources().getDisplayMetrics().widthPixels;
    }
複製代碼

經過上文的構思,CoordinatorRecyclerView暴露滾動,「甩」 的接口方法:

public interface OnCoordinatorListener {
        /** * @param y 相對RecyclerView的距離 * @param deltaY 偏移量 * @param maxParentScrollRange 最大滾動距離 */
        void onScroll(float y, float deltaY, int maxParentScrollRange);

        /** * @param velocityY y方向速度 */
        void onFiling(int velocityY);
    }
複製代碼

重寫onTouchEvent方法:

@Override
    public boolean onTouchEvent(MotionEvent e) {
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 重置數據 因爲篇幅緣由 省略相應代碼 ......
                break;
            case MotionEvent.ACTION_MOVE:
                // y 相對於 RecyclerView y座標
                float y = e.getY();
                measureRecyclerHeight(y);

                if (mLastRawY == 0) {
                    mLastRawY = e.getRawY();
                }

                mDeltaRawY = mLastRawY - e.getRawY();

                if (mIsExpand) {
                    // 展開
                    mListener.onScroll(y, mDeltaRawY, mMaxParentScrollRange);
                } else {
                    // 收起 canScrollVertically 斷定是否滑動到底部
                    if (!mIsConsumeTouchEvent && !canScrollVertically(-1)) {
                        mIsConsumeTouchEvent = true;
                    }
                    if (mIsConsumeTouchEvent && mDeltaRawY != 0) {
                        mListener.onScroll(y, mDeltaRawY, mMaxParentScrollRange);
                    }
                }

                // 處於非臨界狀態
                mIsConsumeTouchEvent = mCurrentParenScrollY > 0 & mCurrentParenScrollY < mMaxParentScrollRange;
                mVelocityTracker.addMovement(e);
                mLastRawY = e.getRawY();

                if (y < 0 || mIsConsumeTouchEvent) {
                    return false;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                // 重置數據
                resetData();

                mLastRawY = 0;

                // 處理滑動速度
                mVelocityTracker.addMovement(e);
                mVelocityTracker.computeCurrentVelocity(1000);

                int velocityY = (int) Math.abs(mVelocityTracker.getYVelocity());
                mListener.onFiling(mDeltaRawY > 0 ? -velocityY : velocityY);
                mDeltaRawY = 0;
                y = e.getY();

                if (y < 0) {
                    return false;
                }
                break;
        }
        return super.onTouchEvent(e);
    }
複製代碼

能夠看到,ACTION_MOVE事件中經過e.getY()來獲取相對父類的y軸座標,先後兩次e.getRawY()值獲取偏移量,在展開狀態下,暴露接口onScroll方法,在收起狀態下,根據是否滑動到底部且偏移量不爲0,暴露接口onScroll方法;在ACTION_UP事件中獲取手指擡起的速度與方向暴露onFiling接口方法。注意,onTouchEvent方法的返回值,若是返回false,RecyclerView向下傳遞消費事件(不能滑動)。

有一個細節你們是否注意到了,RecyclerView的高度在父類展開,收起過程當中並不同,以下圖,在非徹底展開的狀態下,高度爲綠色+粉絲區域;在徹底展開狀態下,高度爲綠色區域。

08.png
相關代碼以下:

/** * @param y 手指相對RecyclerView的y軸座標 * y <= 0 表示手指已經滑出RecyclerView頂部 */
    private void measureRecyclerHeight(float y) {
        if (y <= 0 && mIsAgainMeasure) {
            if (getHeight() < mMaxParentScrollRange && mIsExpand) {
                mIsAgainMeasure = false;
                getLayoutParams().height = getHeight() + mMaxParentScrollRange;
                requestLayout();
            }
        }
    }
    
    // 重置高度
    public void resetRecyclerHeight() {
        if (getHeight() > mMaxParentScrollRange && mIsExpand && mIsAgainMeasure) {
            getLayoutParams().height = getHeight() - mMaxParentScrollRange;
            requestLayout();
        }
    }
複製代碼

接下來看看父類CoordinatorLinearLayout怎麼寫。

建立CoordinatorLinearLayout

在上文中已經說起到CoordinatorLinearLayout繼承LinearLayout,功能相對簡單,根據CoordinatorRecyclerView暴露的接口方法進行慣性滑動,一樣先是成員變量:

// 是否展開
    private boolean mIsExpand;
    private OverScroller mOverScroller;
    // 快速拋的最小速度
    private int mMinFlingVelocity;
    // 滾動最大距離 = 圖片裁剪控件的高度
    private int mScrollRange;
    // 滾動監聽接口
    private OnScrollListener mListener;
    // 最大展開因子
    private static final int MAX_EXPAND_FACTOR = 6;
    // 滾動時長
    private static final int SCROLL_DURATION = 500;
複製代碼

構造方法,相關變量的初始化:

public CoordinatorLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mOverScroller = new OverScroller(context);
        mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
        // 設置默認值 = 圖片裁剪控件的寬度
        mScrollRange = context.getResources().getDisplayMetrics().widthPixels;
    }
複製代碼

onScroll方法:

/** * @param y 相對RecyclerView的距離 * @param deltaY 偏移量 * @param maxParentScrollRange 最大滾動距離 */
    public void onScroll(float y, float deltaY, int maxParentScrollRange) {
        int scrollY = getScrollY();
        int currentScrollY = (int) (scrollY + deltaY);

        if (mScrollRange != maxParentScrollRange) {
            mScrollRange = maxParentScrollRange;
        }

        // 越界檢測
        if (currentScrollY > maxParentScrollRange) {
            currentScrollY = maxParentScrollRange;
        } else if (currentScrollY < 0) {
            currentScrollY = 0;
        }

        // 處於展開狀態
        if (y <= 0) {
            setScrollY(currentScrollY);
        } else if (y > 0 && scrollY != 0) { // 處於收起狀態
            setScrollY(currentScrollY);
        }

        if (mListener != null) {
            mListener.onScroll(getScrollY());
        }
    }
複製代碼

先獲取到y軸方向滑動值,而後滑動值的最大最小斷定,最後根據展開,收起狀態設置滑動值並同時暴露滑動值。

onFiling方法:

/** * @param velocityY y方向速度 */
    public void onFiling(int velocityY) {
        int scrollY = getScrollY();
        // 斷定非臨界狀態
        if (scrollY != 0 && scrollY != mScrollRange) {

            // y軸速度是否大於最小拋速度
            if (Math.abs(velocityY) > mMinFlingVelocity) {
                if (velocityY > mScrollRange || velocityY < -mScrollRange) {
                    startScroll(velocityY > mScrollRange);
                } else {
                    collapseOrExpand(scrollY);
                }
            } else {
                collapseOrExpand(scrollY);
            }
        }
    }
複製代碼

在手指擡起時,先獲取y軸方向滑動值,在展開與收起的過程中,根據RecyclerView返回的y方向速度與 ==「甩」== 的最小值比較。若是小於最小值,則根據滑動值進行慣性滑動;反之,大於最小值,並在(mScrollRange , -mScrollRange)區間以外,分別展開與收起,在區間之類一樣根據滑動值進行慣性滑動。

/** * 展開或收起 * * @param scrollY */
    private void collapseOrExpand(int scrollY) {
        // MAX_EXPAND_FACTOR = 6
        int maxExpandY = mScrollRange / MAX_EXPAND_FACTOR;
        if (isExpanding()) {
            startScroll(scrollY < maxExpandY);
        } else {
            startScroll(scrollY < (mScrollRange - maxExpandY));
        }
    }
複製代碼

在展開與收起狀態下,根據滑動值scrollY是否大於指定值來控制展開與收起。

/** * 開始滾動 * * @param isExpand 是否展開 */
    private void startScroll(boolean isExpand) {
        mIsExpand = isExpand;

        if (mListener != null) {
            mListener.isExpand(isExpand);
            if (mIsExpand) {
                // 必須保證滾動完成 再觸發回調
                postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mListener.completeExpand();
                    }
                }, SCROLL_DURATION);
            }
        }

        if (!mOverScroller.isFinished()) {
            mOverScroller.abortAnimation();
        }

        int dy = isExpand ? -getScrollY() : mScrollRange - getScrollY();
        // SCROLL_DURATION = 500
        mOverScroller.startScroll(0, getScrollY(), 0, dy, SCROLL_DURATION);
        postInvalidate();
    }
複製代碼

首先根據isExpand暴露isExpand接口方法,在展開狀態下而且慣性滾動完成時暴露completeExpand接口方法,而後根據是否展開獲取滾動值,最後調用mOverScroller.startScroll方法進行慣性滾動並重寫computeScroll方法:

@Override
    public void computeScroll() {
        // super.computeScroll();
        if (mOverScroller.computeScrollOffset()) {
            setScrollY(mOverScroller.getCurrY());
            postInvalidate();
        }

    }
複製代碼

相關接口方法以下:

public interface OnScrollListener {
        void onScroll(int scrollY);
        /** * @param isExpand 是否展開 */
        void isExpand(boolean isExpand);
        // 徹底展開
        void completeExpand();
    }
複製代碼

CoordinatorRecyclerView與CoordinatorLinearLayout接口實現以下:

// 實現回調接口
        mRecyclerView.setOnCoordinatorListener(new CoordinatorRecyclerView.OnCoordinatorListener() {
            @Override
            public void onScroll(float y, float deltaY, int maxParentScrollRange) {
                mCoordinatorLayout.onScroll(y, deltaY, maxParentScrollRange);
            }

            @Override
            public void onFiling(int velocityY) {
                mCoordinatorLayout.onFiling(velocityY);
            }
        });

        mCoordinatorLayout.setOnScrollListener(new CoordinatorLinearLayout.OnScrollListener() {
            @Override
            public void onScroll(int scrollY) {
                mRecyclerView.setCurrentParenScrollY(scrollY);
            }

            @Override
            public void isExpand(boolean isExpand) {
                mRecyclerView.setExpand(isExpand);
            }

            @Override
            public void completeExpand() {
                mRecyclerView.resetRecyclerHeight();
            }
        });
複製代碼

到這裏,聯動效果就差很少實現了,先來看看效果:

在這裏插入圖片描述
在感覺絲滑的過程當中,發現了一個很奇怪的問題。以下圖:
在這裏插入圖片描述
問題:點擊RecyclerView的子view,點擊事件失效。猜想, 滑出了RecyclerView區域,事件ACTION_CANCEL執行,致使再次點擊RecyclerView區域,事件標誌Flag重置的緣由形成 。同時新草app也一樣存在改問題,小紅書卻不存在。具體緣由小編會具體跟進事件傳遞機制,查找相關資料給出你們一個合理的解釋。

下面,小編給出本身的兼容方案,既然可以拿到RecyclerView觸摸點的座標,那麼能夠經過座標斷定在哪一個RecyclerView的子view中,而後調用performClick方法,模擬點擊事件:

/** * @param recyclerView * @param touchX * @param touchY */
    public void handlerRecyclerInvalidClick(RecyclerView recyclerView, int touchX, int touchY) {
        if (recyclerView != null && recyclerView.getChildCount() > 0) {
            for (int i = 0; i < recyclerView.getChildCount(); i++) {
                View childView = recyclerView.getChildAt(i);
                if (childView != null) {
                    if (childView != null && isTouchView(touchX, touchY, childView)) {
                        childView.performClick();
                        return;
                    }
                }
            }
        }
    }

    // 觸摸點是否view區域內
    private boolean isTouchView(int touchX, int touchY, View view) {
        Rect rect = new Rect();
        view.getGlobalVisibleRect(rect);
        return rect.contains(touchX, touchY);
    }
複製代碼

好了,本篇文章到此結束,明天是婦女節,祝程序員嫂子節日快樂!

有錯誤的地方請指出,多謝~

Github地址:https://github.com/HpWens/MeiWidgetView 歡迎 star

在這裏插入圖片描述

掃一掃 關注個人公衆號
新號但願你們可以多多支持我~
相關文章
相關標籤/搜索