第一站小红书图片裁剪控件之二,自定义CoordinatorLayout联动效果

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012551350/article/details/88173578

本篇续:

第一站小红书图片裁剪控件,深度解析大厂炫酷控件

先来看看几张效果图:
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
emmmm,想感受高清丝滑的动画效果,有以下两种方式:

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

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

在前篇中已经讲了相关手势的处理,本篇重点讲解留白,列表联动效果。

在上一篇中由于篇幅原因,图片左下角裁剪状态的切换并没有讲解,通过分析小红书,有以下4种状态:
图1图2在这里插入图片描述在这里插入图片描述
分别对应:裁切,填满,留白,充满。这里的裁切,填满(是楼主大大取的中文名字,不一定准确),他们分别对应图1,图2。那么4种状态怎么控制图片的显示?

  • 裁切,改变图片的显示区域,在前文中已经提到图片有任意尺寸,默认显示的区域为宽高相等的矩形区域(正方形区域),而在裁切状态下,显示的区域为宽高不相等的区域。以最小边为基准,剩余的一边缩至原来的四分之三,那么什么又是基准呢?这里以简单的公式来理解:
 图片宽度 = a 
 图片高度 = b

如果 a > b 则以宽度为基准,反之以高度有基准。以demo中的图片为例:
ic_gril.png
图片分辨率为 360*240 宽大于高的图片,那么以宽度为基准,控件高度缩放四分之三,最后裁切的效果如下:
在这里插入图片描述

  • 留白,在图片四周有白边,保证图片一边铺满控件,另一边出现白边,白边的区域大小与图片的实际尺寸有关。

  • 填满,与充满同为默认状态,铺满控件,显示区域为宽高相等的矩形区域(正方形区域)。

对了,这里有一点需要说明,裁切状态下控件一边缩放至四分之三长度,与小红书是有差异的,小红书是根据图片实际尺寸改变裁切区域,取的最小值才是四分之三。

构思代码

裁切

裁切,本质就是改变控件显示区域,那么怎么改变控件显示区域,大家一定会想到改变控件大小,对自定义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

qrcode_for_gh_232b5a56667d_258.jpg

扫一扫 关注我的公众号
新号希望大家能够多多支持我~

没有更多推荐了,返回首页