Android - 自定义ScrollLayout
View
scrollTo:相对于初始位置
scrollBy:相对于当前位置
1.滚动对象是View内部的内容
2.X轴 负值:向右,正值:向左 (左正右负)
Y轴 负值:向下,正值:向上
3.滚动效果为跳跃式,没有平滑滚动效果
Scroller使用步骤:
1.创建Scroller实例
2.调用Scroller.startScoll()方法,传入起始位置与滑动偏移量
3.调用View.invalidate()
4.Scroller不负责滑动View, 只负责计算滑动值(数学引擎), View的invalidate()中将回调onComputeScroll(), 可以在该方法中利用Scroller执行滚动逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <?xml version= "1.0" encoding= "utf-8" ?> <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:id= "@+id/container" android:layout_width= "match_parent" android:layout_height= "match_parent" android:paddingBottom= "@dimen/activity_vertical_margin" android:paddingLeft= "@dimen/activity_horizontal_margin" android:paddingRight= "@dimen/activity_horizontal_margin" android:paddingTop= "@dimen/activity_vertical_margin" > <com.yizhui.customviewdemo.ScrollLayout android:layout_width= "match_parent" android:layout_height= "200dp" android:padding= "2dp" android:background= "#515151" > <TextView android:layout_width= "match_parent" android:layout_height= "match_parent" android:gravity= "center" android:text= "First Button" android:background= "#ff0000" /> <TextView android:layout_width= "match_parent" android:layout_height= "match_parent" android:gravity= "center" android:text= "Second Button" android:background= "#ff00ff" /> <TextView android:layout_width= "match_parent" android:layout_height= "match_parent" android:gravity= "center" android:text= "Third Button" android:background= "#eeee00" /> </com.yizhui.customviewdemo.ScrollLayout> </LinearLayout> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | package com.yizhui.customviewdemo; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.animation.AccelerateInterpolator; import android.widget.FrameLayout; import android.widget.Scroller; /** * Created by Yizhui on 2016/6/9. */ public class ScrollLayout extends FrameLayout { /** * 用于计算滚动数值的实例 */ private Scroller mScroller; /** * 可滚动内容的左右边界 */ private int mLeftBound, mRightBound; /** * 判定为拖动的最小移动像素数 */ private int mTouchSlop; /** * 手指移动时所处屏幕的X坐标 */ private int mLastX; /** * 手指按下时所处屏幕的X坐标 */ private int mDownX; public ScrollLayout(Context context) { this (context, null ); } public ScrollLayout(Context context, AttributeSet attrs) { this (context, attrs, 0 ); } public ScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); mScroller = new Scroller(context, new AccelerateInterpolator()); mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); } @Override protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) { super .onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout( boolean changed, int left, int top, int right, int bottom) { if (changed) { int childCount = getChildCount(); for ( int i = 0 ; i < childCount; i++) { View child = getChildAt(i); //为每个子控件进行水平方法的布局 child.layout(getMeasuredWidth() * i, 0 , (i + 1 ) * getMeasuredWidth(), getMeasuredHeight()); } } mLeftBound = getChildAt( 0 ).getLeft(); mRightBound = getChildAt(getChildCount() - 1 ).getRight(); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mDownX = ( int ) ev.getRawX(); mLastX = mDownX; break ; case MotionEvent.ACTION_MOVE: if (Math.abs(ev.getRawX() - mDownX) > mTouchSlop) { return true ; } } return super .onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { int curX, deltaX; switch (event.getAction()) { case MotionEvent.ACTION_MOVE: curX = ( int ) event.getRawX(); deltaX = mLastX - curX; //左滑为正,右滑为负 mLastX = curX; //Log.d("Scroll", deltaX + ", "+curX+", " + getScrollX() + ", "); //不能继续左滑 if (getScrollX() + deltaX > mRightBound - getWidth()) { scrollTo(mRightBound - getWidth(), 0 ); return true ; } //不能继续右滑 if (getScrollX() + deltaX < mLeftBound) { scrollTo( 0 , 0 ); return true ; } scrollBy(deltaX, 0 ); return true ; case MotionEvent.ACTION_UP: boolean isScrollToLeft=mDownX-event.getRawX()> 0 ? true : false ; //滑动1/4 //int targetIndex=(getScrollX()+getWidth()*3/4)/getWidth(); /* * 滑动1/4视为滑动上下块,若当前index=1 * 左滑: ((1+x)+3/4) 取整 , x=[0,1] -> f=[1,2] 1/4处为临界点 * 右滑: ((1-x)+1/4) 取整 , x=[0,1] -> f=[1,0] 1/4处为临界点 */ int targetIndex=( int )((getScrollX()+(isScrollToLeft?( 3.0 / 4 ):( 1.0 / 4 ))*getWidth())/getWidth()); mScroller.startScroll(getScrollX(), 0 , targetIndex * getWidth() - getScrollX(), 0 ); invalidate(); break ; } return true ; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } } } |
代码说明:
1、ScrollLayout 重载onInterceptTouchEvent,拦截ACTION_MOVE事件
2、ScrollLayout 需要消费ACTION_DOWN事件,因为子View可能不会消费ACTION_DOWN(如:TextView并且没有设置clickable为true)
3、startScroll()中关于滑动偏移量的计算公式为:targetIndex * getWidth() - getScrollX() ,可分为如下四种情况分析:
左滑:满足条件,继续左滑进入下一个,offset= targetX-curX
不满足条件,右滑进行还原,offset=targetX-curX
右滑:同上
结果演示:
另附:
参考文章:
Android Scroller完全解析,关于Scroller你所需知道的一切
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
· ASP.NET Core 模型验证消息的本地化新姿势
· 开发的设计和重构,为开发效率服务
· 从零开始开发一个 MCP Server!
· Ai满嘴顺口溜,想考研?浪费我几个小时
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· .NET 原生驾驭 AI 新基建实战系列(一):向量数据库的应用与畅想