最终效果如下图:在一个 Activity 中横向展示多个页面,每个页面都有自己的 ListView 列表,当手指上下滑动的距离大于横向滑动的距离时,就滚动当前页面的 ListView 列表项,当手指上下滑动的距离小于横向滑动的距离时,就切换页面,类似 ViewPager
下面分成两部分说明一下,①测量和布局;②事件分发
PART_A 测量和布局
-
先看下我们这个容器控件在Activity中的具体情况
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <cc.catface.helloworld.view.MyHorizontalScrollView android:id="@+id/mhsv_01" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
从上可得重要信息:自定义ViewGroup的宽/高设置都为match_parent即对应的LayoutParams为EXACTLY
-
测量onMeasure()
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 记录当前ViewGroup的测量宽/高值 int measureedWidth; int measureedHeight; int childCount = getChildCount(); measureChildren(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (childCount == 0) { setMeasuredDimension(0, 0); } else { View childView = getChildAt(0); // 当子(元素)页面宽/高设置为wrap_content时,自行计算子(元素)页面的宽/高值 // 当前ViewGroup的宽即单个子页面宽值乘以子页面个数 // 在此由于当前ViewGroup的宽/高设置都为match_parent,所以最后交给系统的整体控件的宽/高值均为父容器即所在Activity页面的建议值 measureedWidth = childView.getMeasuredWidth() * childCount; measureedHeight = childView.getMeasuredHeight(); setMeasuredDimension((widthSpecMode == MeasureSpec.AT_MOST) ? measureedWidth : widthSpecSize, (heightSpecMode == MeasureSpec.AT_MOST) ? measureedHeight : heightSpecSize); } }
-
布局onLayout()
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int left = 0; int childCount = getChildCount(); mChildrenSize = childCount; for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); if (childView.getVisibility() != View.GONE) { // 即父容器的建议宽/高值match_parent int childWidth = childView.getMeasuredWidth(); int childHeight = childView.getMeasuredHeight(); mChildWidth = childWidth; childView.layout(left, 0, left + childWidth, childHeight); // 为后续子页面的左起点坐标做处理 left += childWidth; } } }
PART_B 事件分发
-
初始化
public class MyHorizontalScrollView extends ViewGroup { private int mChildrenSize; private int mChildWidth; private int mChildIndex; // 记录上次滑动的坐标 private int mLastX = 0; private int mLastY = 0; // 记录上次滑动的坐标(onInterceptTouchEvent) private int mLastXIntercept; private int mLastYIntercept; private Scroller mScroller; private VelocityTracker mVelocityTracker; public MyHorizontalScrollView(Context context) { super(context); init(); } public MyHorizontalScrollView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { if (mScroller == null) { mScroller = new Scroller(getContext()); mVelocityTracker = VelocityTracker.obtain(); } } ...... }
-
onInterceptTouchEvent
@Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { // 触屏瞬间拦截由父容器处理事件 case MotionEvent.ACTION_DOWN: intercepted = false; if (!mScroller.isFinished()) { mScroller.abortAnimation(); // 可省,优化滑动体验 intercepted = true; } break; // 根据水平/竖直滑动距离的大小控制切页面还是滚动当前页面列表 case MotionEvent.ACTION_MOVE: int deltaX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; if (Math.abs(deltaX) > Math.abs(deltaY + 40)) { intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } mLastX = x; mLastY = y; mLastXIntercept = x; mLastYIntercept = y; return intercepted; }
-
onTouchEvent
@Override public boolean onTouchEvent(MotionEvent event) { mVelocityTracker.addMovement(event); int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (!mScroller.isFinished()) { mScroller.abortAnimation(); } break; // 滑动页面 case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; int deltaY = y - mLastY; scrollBy(-deltaX, 0); break; // 根据水平滑动的方向和速度大小确定向左或向右切换页面 case MotionEvent.ACTION_UP: int scrollX = getScrollX(); mVelocityTracker.computeCurrentVelocity(1000); float xVelocity = mVelocityTracker.getXVelocity(); if (Math.abs(xVelocity) >= 50) { mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1; } else { mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth; } // 控制滑动的页面在正确范围内 mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1)); // 手指离屏瞬间控制滑动方向和距离 int dx = mChildIndex * mChildWidth - scrollX; smoothScrollBy(dx, 0); mVelocityTracker.clear(); break; default: break; } mLastX = x; mLastY = y; return true; }
-
滑动
private void smoothScrollBy(int dx, int dy) { // int startX, int startY, int dx, int dy, int duration mScroller.startScroll(getScrollX(), 0, dx, 0, 500); invalidate(); }
// 光滑滑动 @Overridepublic void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }
-
内存回收
@Override protected void onDetachedFromWindow() { mVelocityTracker.clear(); mVelocityTracker.recycle(); super.onDetachedFromWindow(); }
完整类如下
public class MyHorizontalScrollView extends ViewGroup {
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
// 记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
// 记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept;
private int mLastYIntercept;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public MyHorizontalScrollView(Context context) {
super(context);
init();
}
public MyHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
if (mScroller == null) {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY + 40)) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
private void smoothScrollBy(int dx, int dy) {
// int startX, int startY, int dx, int dy, int duration
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 记录子View测量宽/高值
int measureedWidth;
int measureedHeight;
int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else {
View childView = getChildAt(0);
// 当子(元素)页面宽/高设置为wrap_content时,自行计算子(元素)页面的宽/高值
// 整体布局宽即单个子页面宽值乘以子页面个数
// 在此由于当前控件的宽/高设置都为match_parent,所以最后交给系统整体控件的宽/高值均为父容器即所在Activity页面的建议值
measureedWidth = childView.getMeasuredWidth() * childCount;
measureedHeight = childView.getMeasuredHeight();
setMeasuredDimension((widthSpecMode == MeasureSpec.AT_MOST) ? measureedWidth : widthSpecSize, (heightSpecMode == MeasureSpec.AT_MOST) ? measureedHeight : heightSpecSize);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = 0;
int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
// 即父容器的建议宽/高值match_parent
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
mChildWidth = childWidth;
childView.layout(left, 0, left + childWidth, childHeight);
// 为后续子页面的左起点坐标做处理
left += childWidth;
}
}
}
@Override
protected void onDetachedFromWindow() {
mVelocityTracker.clear();
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}