第一站小红书图片裁剪控件之二,自定义CoordinatorLay
本篇续:
先来看看几张效果图:
![](https://img.haomeiwen.com/i2258857/fe52258af3852da7.gif)
![](https://img.haomeiwen.com/i2258857/0165da15a51947f8.gif)
![](https://img.haomeiwen.com/i2258857/74210c92a43a5a08.gif)
![](https://img.haomeiwen.com/i2258857/f1b06317a4e5f084.gif)
emmmm,想感受高清丝滑的动画效果,有以下两种方式:
https://github.com/HpWens/MeiWidgetView 欢迎Star
https://www.pgyer.com/zKF4 APK地址
在前篇中已经讲了相关手势的处理,本篇重点讲解留白,列表联动效果。
在上一篇中由于篇幅原因,图片左下角裁剪状态的切换并没有讲解,通过分析小红书,有以下4种状态:
![](https://img.haomeiwen.com/i2258857/2b53b9f335bc2b3b.png)
![](https://img.haomeiwen.com/i2258857/19c5d5bc691e775d.png)
![](https://img.haomeiwen.com/i2258857/28cd34a5e4189888.png)
![](https://img.haomeiwen.com/i2258857/79c46e6967cadb22.png)
分别对应:裁切,填满,留白,充满。这里的裁切,填满(是楼主大大取的中文名字,不一定准确),他们分别对应图1,图2。那么4种状态怎么控制图片的显示?
- 裁切,改变图片的显示区域,在前文中已经提到图片有任意尺寸,默认显示的区域为宽高相等的矩形区域(正方形区域),而在裁切状态下,显示的区域为宽高不相等的区域。以最小边为基准,剩余的一边缩至原来的四分之三,那么什么又是基准呢?这里以简单的公式来理解:
图片宽度 = a
图片高度 = b
如果 a > b 则以宽度为基准,反之以高度有基准。以==demo==中的图片为例:
![](https://img.haomeiwen.com/i2258857/1ff034fa9d30d8c2.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( 控件宽度/图片宽度 , 控件高度/图片高度 )
编写代码
裁切
裁切分为以下两步:
- 判定宽或高为基准边:
// 获取图片的宽度和高度
Drawable drawable = getDrawable();
if (null == drawable) {
return;
}
int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight();
// mIsWidthLarger true 宽度有基准边 高度裁剪 false 高度为基准边 宽度裁剪
mIsWidthLarger = drawableWidth > drawableHeight;
- 重写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);
}
有不明白的地方,请参考注释或留言,效果图就像这样:
![](https://img.haomeiwen.com/i2258857/1cb953384690b441.gif)
留白
填满、充满为默认状态,在前篇已经讲解过了。留白,一边留白一边铺满,那么图片的缩放比例就会发生改变,还记得前篇中的缩放比例吗:
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);
}
上下越界的情况同左右越界的情况,好了,来看下效果图:
![](https://img.haomeiwen.com/i2258857/860c32e5329557ea.gif)
缓存,压缩,保存裁剪图片
缓存
有关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);
}
图片裁剪控件还有一些细节,这里就不一一讲解了,有什么疑问,欢迎留言讨论?接下来重点讲解列表联动效果。
列表联动
![](https://img.haomeiwen.com/i2258857/fc11ee9bd06d370f.gif)
相信大家第一眼看到这个效果,一定会想到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>
-
未触碰MCropImageView区域,RecyclerView消费滑动事件,滚动列表
-
在RecyclerView区域向上滑动,触碰到MCropImageView区域,RecyclerView与MCropImageView跟随手指移动,向上滑动移出屏幕;向下滑动则移入屏幕,当MCropImageView完全展示,MCropImageView停止移动,如果手指移动到RecyclerView区域,则消费滑动事件。
收起状态:
-
未滑动到RecyclerView顶部,RecyclerView自身消费滑动事件
-
滑动到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的高度在父类展开,收起过程中并不一样,如下图,在非完全展开的状态下,高度为绿色+粉丝
区域;在完全展开状态下,高度为绿色
区域。
![](https://img.haomeiwen.com/i2258857/3225cb9e32e80d31.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();
}
});
到这里,联动效果就差不多实现了,先来看看效果:
![](https://img.haomeiwen.com/i2258857/1cbb1a12d52ac4eb.gif)
在感受丝滑的过程中,发现了一个很奇怪的问题。如下图:
![](https://img.haomeiwen.com/i2258857/4f41f11ab3ff8adf.gif)
问题:点击RecyclerView的子view,点击事件失效。猜测,
下面,小编给出自己的兼容方案,既然能够拿到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
![](https://img.haomeiwen.com/i2258857/27f4e8c3d79e6204.jpg)
扫一扫 关注我的公众号:控件人生
新号希望大家能够多多支持我~