Android 自定义布局竖向的ViewPager的实现
效果图:
这个自定义控件涉及到的知识点:
自定义ViewGroup中onMeasure和onLayout的写法
弹性滚动Scroller的用法
速度轨迹追踪器VelocityTracker的用法
如何处理滑动事件冲突
dispatchTouchEvent:(外部拦截)告诉此ScrollLayout的父布局,什么时候该拦截触摸事件,什么时候不该拦截触摸事件
onInterceptTouchEvent:(内部拦截)ScrollLayout告诉自己什么时候要拦截内部子View的触摸事件,什么时候不要拦截内部子View的触摸事件
处理触摸滑动的思路:
//即确定当前显示的子控件的位置, //确定弹性滑动滑向那个位置 if (Math.abs(velocityY) > criticalVelocityY) {//当手指滑动速度快时,按照速度方向直接翻页 // 重点二、快速滑动时,如何判断当前显示的是第几个控件,并且再次包含边界判断(必须包含边界判断,因为前面的边界判断,只适用于低速滑动时) if (shouZhiXiangXiaHuaDong) { if (currentPage > 1) {//★★★★★★★★边界限制,防止滑倒第一个,还继续滑动,注意★(currentPage-2) mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 2) - getScrollY()); currentPage--; } } else { if (currentPage
总结
当要写一个算法时,不要着急试图一下子写出来,这样往往陷入盲目,应该是一步一步地推导,一步一步实现代码,指导最后找到规律,类似于归纳总结、通项公式的方法。
代码如下:(注释很全)
package beautlful.time.com.beautlfultime.view; import android.content.Context; import android.support.v4.view.ViewConfigurationCompat; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.Scroller; /** * 注意:此自定义viewgroup只适用于每个子控件为match_parent的情况,其实一般情况也都是这种情况 * 注意:此自定义viewgroup,没有考虑padding的情况,使用者不要在ScrollerLayout里使用任何padding,否则你看到的不是你想要的, * 为了实现padding效果,你可以为ScrollerLayout的外层再套一层线性布局(或其他布局),在外层布局里使用padding值 * 此自定义viewgroup基于郭霖博客改编,想了解具体实现细节,请参照: * Android Scroller完全解析,关于Scroller你所需知道的一切 * http://blog.csdn.net/guolin_blog/article/details/48719871 */ public class VerticalViewPager extends ViewGroup { int currentPage = 1; /** * 速度轨迹追踪器 */ private VelocityTracker mVelocityTracker; /** * 此次计算速度你想要的最大值 */ private final int mMaxVelocity; /** * 第一个触点的id, 此时可能有多个触点,但至少一个 */ private int mPointerId; /** * 计算出的竖向滚动速率 */ private float velocityY; /** * 手指横向滑动的速率临界值,大于这个值时,不考虑手指滑动的距离,直接滚动到最左边或者最右边 */ private int criticalVelocityY = 2500; /** * 用于完成滚动操作的实例 */ private Scroller mScroller; /** * 判定为拖动的最小移动像素数 */ private int mTouchSlop; /** * 手机按下时的屏幕坐标 */ private float mYDown; /** * 手机当时所处的屏幕坐标 */ private float mYMove; /** * 上次触发ACTION_MOVE事件时的屏幕坐标 */ private float mYLastMove; /** * 界面可滚动的顶部边界 */ private int topBorder; /** * 界面可滚动的底部边界 */ private int bottomBorder; /** * 子控件的高度(这里每个子控件高度都一样,都是match_parent) */ private int childHeight; /** * 手指是否是向下滑动 */ private boolean shouZhiXiangXiaHuaDong; private int childCount; public VerticalViewPager(Context context, AttributeSet attrs) { super(context, attrs); // 第一步,创建Scroller的实例 mScroller = new Scroller(context); ViewConfiguration cOnfiguration= ViewConfiguration.get(context); // 获取TouchSlop值 mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); //此次计算速度你想要的最大值 mMaxVelocity = ViewConfiguration.get(context).getMaximumFlingVelocity(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); childCount = getChildCount(); for (int i = 0; imTouchSlop) { return true; } break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { //▲▲▲2.向VelocityTracker添加MotionEvent acquireVelocityTracker(event); switch (event.getAction()) { case MotionEvent.ACTION_MOVE: //▲▲▲3.求伪瞬时速度 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); velocityY = mVelocityTracker.getYVelocity(mPointerId); mYMove = event.getRawY(); int scrolledY = (int) (mYLastMove - mYMove);//注意取的是负值,因为是整个布局在动,而不是控件在动 if (getScrollY() + scrolledY bottomBorder) {//如果已经在最底部了,就不让再往底部滑动了 scrollTo(0, bottomBorder - getHeight()); return true;//★★★★★★★★★★★★★★★★★这里返回true或者false实践证明都可以,但是不能什么都不返回。 } scrollBy(0, scrolledY);//手指move时,布局跟着滚动 if (mYDown <= mYMove) {//★★★判断手指向上滑动,还是向下滑动,要用mYDown,而不是mYLastMove shouZhiXiangXiaHuaDOng= true;//手指往下滑动 } else { shouZhiXiangXiaHuaDOng= false;//手指往上滑动 } mYLastMove = mYMove; break; case MotionEvent.ACTION_UP: // 4.▲▲▲释放VelocityTracker releaseVelocityTracker(); // 第二步,当手指抬起时,根据当前的滚动值以及滚动方向来判定应该滚动到哪个子控件的界面,并且记得调用invalidate(); if (Math.abs(velocityY) > criticalVelocityY) {//当手指滑动速度快时,按照速度方向直接翻页 // 重点二、快速滑动时,如何判断当前显示的是第几个控件,并且再次包含边界判断(必须包含边界判断,因为前面的边界判断,只适用于低速滑动时) if (shouZhiXiangXiaHuaDong) { if (currentPage > 1) {//★★★★★★★★边界限制,防止滑倒第一个,还继续滑动,注意★(currentPage-2) mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 2) - getScrollY()); currentPage--; } } else { if (currentPage = childHeight * (currentPage - 1) + childHeight / 2 && !shouZhiXiangXiaHuaDong)) { // 手指向上滑动并且,滚动距离过了屏幕一半的距离 mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage) - getScrollY()); currentPage++; } else if ((getScrollY() childHeight * (currentPage - 2) + childHeight / 2 && shouZhiXiangXiaHuaDong)) { // 手指向下滑动并且,滚动距离没有过屏幕一半的距离 mScroller.startScroll(0, getScrollY(), 0, childHeight * (currentPage - 1) - getScrollY()); } /* if ((getScrollY() >= childHeight && !shouZhiXiangXiaHuaDong)//手指往左滑动,并且滑动完全显示第二个控件时,viewgroup滑动到最右端 || ((getScrollY() >= (totalChildHeight - firstChildHeight - lastChildHeight) && shouZhiXiangXiaHuaDong))) {//手指往右滑动,并且当滑动没有完全隐藏最后一个控件时,viewgroup滑动到最右端 // 当滚动值大于某个数字时(大于第二个控件的宽度,即完全显示第二个控件时)并且是向左滑动,让这个viewgroup滑动到整个Viewgroup的最右侧, // 因为右侧的所有控件宽度是600,而现在已经滑动的距离是getScrollX, // 那么,还应该继续滑动的距离是600-getScrollX(),这里正值表示向右滑动 mScroller.startScroll(0,getScrollY(), 0, (totalChildHeight - firstChildHeight) - getScrollY()); } else if ((getScrollY() <= (totalChildHeight - firstChildHeight - lastChildHeight) && shouZhiXiangXiaHuaDong)//手指往右滑动,并且当滑动完全隐藏最后一个控件时,viewgroup滑动到最左端 || (getScrollY() <= childHeight && !shouZhiXiangXiaHuaDong)) {//手指往左滑动,并且滑动没有完全显示第二个控件时,viewgroup滑动到最左端 // 当滚动值小于某个数字时,让这个viewgroup滑动到整个Viewgroup的最左侧, // 因为滑动到最左侧时,就是让整个viewgroup的滑动量为0,而现在已经滑动的距离是getScrollX, // 那么,还应该继续滑动的距离是0-getScrollX(),这里负值表示向左滑动 mScroller.startScroll(0,getScrollY(), 0, 0 - getScrollY()); }*/ } // 必须调用invalidate()重绘 invalidate(); break; case MotionEvent.ACTION_CANCEL: // 5.▲▲▲释放VelocityTracker releaseVelocityTracker(); break; } return super.onTouchEvent(event); } @Override public void computeScroll() { // 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑 if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } } /** * @param event 向VelocityTracker添加MotionEvent * @see VelocityTracker#obtain() * @see VelocityTracker#addMovement(MotionEvent) */ private void acquireVelocityTracker(final MotionEvent event) { if (null == mVelocityTracker) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); } /** * 释放VelocityTracker * * @see VelocityTracker#clear() * @see VelocityTracker#recycle() */ private void releaseVelocityTracker() { if (null != mVelocityTracker) { mVelocityTracker.clear(); mVelocityTracker.recycle(); mVelocityTracker = null; } } /* getScrollX()指的是由viewgroup调用View的scrollTo(int x, int y)或者scrollBy(int x, int y)产生的X轴的距离 // 换句话说,就是你手指每次滑动,引起的是viewgroup累计滑动的距离,右为正 // 指的是相当于控件的左上角的为原点的坐标值 Log.e("qqq","getX():"+event.getX()); // 指的是相当于屏幕的左上角的为原点的坐标值 Log.e("qqq","getRawX():"+event.getRawX());*/ }
布局文件
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!