需求
想做一个卡片堆叠效果的滑动,两个视图,滑动过程将第二个view叠加在第一个view上边,形成叠加的效果,有点像 NestedScrollView + CoordinatorLayout + Toolbar 的效果。预览图如下:
看起来有点像 NestedScrollView+Toolbar
的效果,只是里面的变换不一样,往上推过程第二个控件往上移动,第一个 view 不发生变换。我这个是自定义viewgroup方式实现,其实用 NestedScroll
也能实现,难度应该会更低,下次再使用 NestedScroll
实现。
分析
使用自定义 ViewGroup
来做这个效果,有两个需要解决的点,一是关于第二个 view 的大小测量,有用过 NestedScrollView
嵌套过 ListView
的同学肯定知道嵌套后 ListView
只显示一行,必须去重写 ListView
的 onMeasure
才能解决显示不完全的问题,如果不指定 ListView
的具体大小,需自行计算第二个 view 的大小;二是触摸事件的分发处理,在滑动过程如果第二 view 没有推到顶,父布局要消费这个事件,如果到顶了,需要把事件继续下发给第二个 view 。
- 测量大小
测量 view 大小这里不展开,ListView
的话在adapterNotifyDataSetChange
后根据父布局已确定的大小重新测量,其他也一样,需要注意的一点是,叠加的视图(即最下面的那个 view ,下面用 target 代替)上移上去,也就是说改变视图的 top 大小,所以在测绘 target 的时候,建议把 target的高度再加上需位移大小,这样移动到最上面的时候,视图最下面不会出现空白区域。 - 使用
MarginLayoutParams
无论是测量大小还是摆放 view ,能用 margin 肯定是最好的,所以需要重写ViewGroup
的public LayoutParams generateLayoutParams(AttributeSet attrs)
方法,返回MarginLayoutParams
,否则子 view 获取 LayoutParams 强转为MarginLayoutParams
会出类转换异常,需注意 - 触摸事件
- 触摸事件分发复习一下大致流程
最上层下发触摸事件,由dispatchTouchEvent
分发,中途由onInterceptTouchEvent
决定是否拦截,拦截的话不再下发,进入拦截view的onTouchEvent
,不拦截的继续下发到子 view ,重复上一步骤分发dispatch
,如果onTouchEvent
消耗了该事件(return true)则不再往上回调onTouchEvent
,否则继续上传回最顶层view,当然,子 view 也可以请求父布局不准消耗触摸事件,强制要求下发,如可滑动的视图,请求父布局public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
是否禁止拦截触摸事件。 - 关于拦截
ACTION_MOVE
不触发问题
这个从逻辑层面来解释比较容易,我们可以理解移动的形成首先由手指按下,再到手指抬起,中间产生的位移,即为移动,那么也就说需要先拦截到ACTION_DOWN
,向系统通知这是一个有效的按压,才会触发下一个 move 事件,没有 down 作为前提,是没有 move 存在的可能,所以需要在拦截ACTION_DOWN
时候返回 true。 - 事件继续下发
当我们滑动到最顶部的时候,这时候的移动对我们来说已经没有用了,需要把这个事件传递给子 view,但是 move 的前提是什么?是ACTION_DOWN
,所以需要在继续下发之前,先主动下发一个ACTION_DOWN
,再去分发ACTION_MOVE
- 触摸事件分发复习一下大致流程
实现
Talk is cheap,show me the code.
-
测量布局的就不写了,不明白可以看看 android 的
LinearLayout/RelativeLayout
等布局写法 -
onLayout
可以参考 LinearLayout ,只是我们需要在摆放 target 视图时把位移高度加进去,如下:@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int count = getChildCount(); int layoutTop = top; int limitFirstChild = 0; for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != View.VISIBLE) { continue; } int width = child.getMeasuredWidth(); int height = child.getMeasuredHeight(); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); left += lp.leftMargin; layoutTop += lp.topMargin; if (i == 0 && limitOffset == 0) { limitFirstChild = layoutTop + height / 2;//留存第一个view的top+高度/2,遮盖一半的视图 } if (i == count - 1