这个示例是学习自定义 ViewGroup 的一个学习示例,总结自定义 ViewGroup 方面的知识。
准备
首先了解自定义一个滑动流畅的 ViewGroup 需要那些知识。
-
基础
-
自定义 ViewGroup onMeasure的处理,主要是让子 View 进行测量,然后根据子View 测量的数据得到本身尺寸。
-
自定义 ViewGoup onLayout 的处理,如何摆放好子 View 只要计算细心一点很简单,记得处理子 View 的 margin以及自身的 padding。
-
事件分发机制,在
onInterceptTouchEvent
中拦截滑动事件和处理滑动相关事务。在
onTouchEvent
中真正的处理滑动。
-
-
几个辅助类的使用
- Scroller 用户处理快速滑动,记住滑的越快View滚得越快越多,主要是几个对应 api 的使用。
- VelocityTracker 检测用户手势的滑动速度用于给
Scroller
使用,记住先初始化,然后添加手势,然后需要获取的时侯先计算再获取。
开始,处理 onMeasure / onLayout
处理 onMeasure
其实和 ViewGroup
的布局相关,当 ViewGroup 的尺寸不是精确的是 wrap_content
那么在处理的时侯就要考虑到在 x/y 轴空间不足时换行或者换列,而换行换列的操作都会影响 ViewGroup
最终的尺寸。
这里实现的是一个流式布局可垂直滑动的 ViewGroup
。
PS: 测量的这里没有考虑到布局因素,请各位看官掌眼!
onMeasure
总体步骤是
- 获取自身测量模式及父View建议的尺寸数据。
- 循环的获取子View,调用
measureChild
方法使子View进行自我测量。 - 根据布局策略计算 ViewGroup 高度。
- 根据测量模式决定最终高度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取自身测量模式及尺寸数据
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int realWidth = 0;
int realHeight = getPaddingTop() + getPaddingBottom();
// 测量子View,根据子view及自身测量模式完成测量
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// 测量 子View
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 获取子View的宽高
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 根据 子 View 的数据计算自身的高度
realHeight = realHeight + childHeight;
}
// 高度最终决定
switch (heightMode) {
// 若设置了精确数值
case MeasureSpec.EXACTLY:
realHeight = heightSize;
break;
// 若是包裹内容
case MeasureSpec.AT_MOST:
if (realHeight > heightSize) {
realHeight = heightSize;
}
break;
}
// 宽度最终决定
switch (widthMode) {
case MeasureSpec.AT_MOST:
realWidth = widthSize;
break;
case MeasureSpec.EXACTLY:
realWidth = widthSize;
break;
}
// 设置最终宽高
setMeasuredDimension(realWidth, realHeight);
}
复制代码
onLayout
布局就简单了,想要子 View 摆成什么样子就给子View什么定位参数,计算精确一点,注意处理 padding 和 margin。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = getMeasuredWidth();
// 已经使用的高度
viewHeight = getMeasuredHeight();
// 下一行开始的 top 位置
int nextTopPoint = getPaddingTop();
// 已经使用的宽度
int useWidth = getPaddingLeft();
// 已经使用的高度
int useHeight = getPaddingTop();
int childCount = getChildCount();
MarginLayoutParams marginLayoutParams;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
Log.e("sun", child.getLayoutParams().getClass().getName());
marginLayoutParams = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
int left = useWidth + marginLayoutParams.leftMargin;
int top = nextTopPoint + marginLayoutParams.topMargin;
int right = left + childWidth + marginLayoutParams.rightMargin;
int bottom = top + childHeight + marginLayoutParams.bottomMargin;
// 是否需要换行
if (right > width) {
left = getPaddingLeft() + marginLayoutParams.leftMargin;
top = top + childHeight + marginLayoutParams.topMargin;
right = left + childWidth + marginLayoutParams.rightMargin;
bottom = top + childHeight + marginLayoutParams.bottomMargin;
nextTopPoint = top;
}
useWidth = right;
viewContentHeight = nextTopPoint;
child.layout(left, top, right, bottom);
}
}
复制代码
处理滑动
Android 中所有的 View 都是支持滑动,系统有提供 scrollBy()
以及 scrollTo
两个方法来处理滑动,两个方法的区别在于一个是相对 view 之前的位置,一个是绝对与 view 的 top。
拦截滑动事件
拦截滑动事件是在 onInterceptTouchEvent
中处理,为什么呢?方便处理滑动冲突,以及避免被子View抢了事件。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int eventKey = ev.getAction();
// 判断是何种事件
switch (eventKey) {
case MotionEvent.ACTION_DOWN:
// 记下按下的屏幕坐标
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
// 如果 scroller 动画没停止,但是用户已经触摸,则该立刻停止
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
lastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float moveY = ev.getY();
float moveSlop = Math.abs(moveY - lastY);
// 滑动值大于最小滑动值则拦截事件
if (moveSlop > mTouchSlop) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
复制代码
处理滑动事件
@Override
public boolean onTouchEvent(MotionEvent event) {
int eventKey = event.getAction();
switch (eventKey) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
mVelocityTracker.addMovement(event);
float moveY = event.getY();
int moveDis = (int) (lastY - moveY);
// 几个参数的意思:需要滑动 x 的距离,需滑动 y 的距离,x 已经滑动的值,y 已经滑动的值,
// x 滑动的范围 ,y 滑动的范围,x 显示边缘效果时的最大值(待验证,可能是回弹间隙),y 显示边缘效果时的最大值(待验证,可能是回弹间隙),执行完滚动之后是否继续处理后续事件(没用到的参数)
overScrollBy(0, moveDis, 0, getScrollY(), 0, getRangY(), 0, 0, true);
// 记录最后一次的坐标
lastY = event.getY();
return true;
case MotionEvent.ACTION_UP:
// 当快速滑动时,使用 scroller 完成流程滑动
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
float velocity = mVelocityTracker.getYVelocity();
Log.e("sun", "速度" + velocity + "默认速度" + mMinimumVelocity);
if (Math.abs(velocity) > mMinimumVelocity) {
mScroller.fling(0, getScrollY(), 0, (int) -velocity, 0, 0, 0, getRangY());
// invalidate();
}
mVelocityTracker.clear();
break;
}
return super.onTouchEvent(event);
}
复制代码
完整代码请戳:gitHub