自定义View详解
1 View与ViewGroup
1.1 继承关系
在Android中,View是所有控件的父类,例如Button、TextView、LinearLayout、RelativeLayout等等。View是一种抽象的概念,代表一个控件。
ViewGroup的父类是View,它可以容纳其他View,可以将它看作一个View容器。如下图所示:LinearLayout继承自ViewGroup,而ViewGroup又继承自View,因此LinearLayout既是ViewGroup也是View。
1.2 View与ViewGroup
View与ViewGroup构成了Android应用的界面。官网是这样解释的:
Android 应用的界面使用布局(ViewGroup
对象)和微件(View
对象)的层次结构构建而成。布局是一种容器,用于控制其子视图在屏幕上的放置方式。微件是界面组件,如按钮和文本框。
1.3 View与ViewGroup的工作流程
- View的工作流程一般涉及到2个方法:
-
onMeasure
:测量View自身的尺寸 -
onDraw
:根据测量的尺寸来绘制自身
- ViewGroup的工作流程一般涉及到3个方法:
-
onMeasure
:测量ViewGroup自身和所有子View的尺寸 -
onLayout
:负责布局,用来确定子View在布局空间中的摆放位置 -
onDraw
:根据测量的尺寸来绘制自身
1.4 View的位置
在Android中,View的位置并不是相对于屏幕,而是相对于View的父容器来说的,并且水平向右为x轴的正方向,竖直向下为y轴的正方向。
如上图所示,黑色边框为屏幕,红色边框为ViewGroup1,绿色边框为ViewGroup2,则ViewGroup2的位置是相对于ViewGroup1的位置来说的,而View(蓝色边框)的位置是相对于ViewGroup2的位置来说的。
View的位置可以用四个属性:Left(左边距)、Top(上边距)、Right(右边距)、Bottom(下边距)方法获取来表示,并且分别通过getLeft、getTop、getRight、getBottom方法来获取。
从Android 3.0开始,View增加了额外的几个参数,分别是:x、y、translationX、translationY。它们之间的换算关系如下:x=left+translationX
,y=top+translationY
应用场景: 在属性动画中,top和left表示原始的左上角位置信息,并不会发生改变,而改变的是translationX和translationY(translationX和translationY默认值为0),因此导致x,y也发生改变。
2 实现自定义View
2.1 使用onDraw方法画一个圆
- 先简单归纳以下思路:
- 新建一个类并继承自View,并创建其构造方法(这里为
CircleView
,并创建所有的构造方法) - 在构造方法中创建一个方法init,在其中做一些初始化工作(创建画笔,设置画笔颜色等等)
- 重写onDraw方法。
- 代码实现:
- CircleVIew的代码如下所示:
public class CircleView extends View { private Paint paint; public CircleView(Context context) { super(context); init(); } public CircleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(); } public void init() { //创建一个抗锯齿的红色画笔 paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(Color.RED); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save();//保存画板 int width = getWidth(); int height = getHeight(); //设置圆的半径为宽和高中较小值的1/2,否则显示不全 int radius = Math.min(width, height) / 2; //x,y都为半径,避免因宽高不一致而造成有margin值 canvas.drawCircle(radius, radius, radius, paint); canvas.restore(); }}
- 然后在布局文件中引入CircleView控件:
为了更好地理解自定义View,这里添加一个灰色的背景色( #808080
)
- 运行结果如下所示:
2.2 实现wrap_content
先来了解下测量模式,在
MeasureSpec
类中定义了三种模式:
UNSPECIFIED
:未指定,父控件对子控件没有任何约束,想要多大就多大,比如:ScrollView。EXACTLY
:确切的值,表示父控件为子控件指定的确定的大小,他对应于LayoutParams中的match_parent和具体的数值。AT_MOST
:至多,一般是父控件为子控件指定了最大值,子控件不要超过他,一般是子控件使用了wrap_content。
- 新建一个类并继承自View(这里为
WrapContentCircleView
),并创建构造方法和onDraw方法(同CircleView
),更改布局文件为:
- 在将上面布局文件中的WrapContentCircleView的
layout_width
属性改为wrap_content
后,发现CircleView的宽仍然填充了父布局,如下图灰色背景。
- 那么如何实现wrap_content效果呢?这时就需要重写父类View的onMeasure方法了。
//由于默认单位为px,这里将其转换为dp,便于与上面的结果进行对比private final int DEFAULT_WIDTH = DensityUtil.dip2px(getContext(), 100);private final int DEFAULT_HEIGHT = DensityUtil.dip2px(getContext(), 150);@Overrideprotected 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 resultWidth = widthSize; int resultHeight = heightSize; //AT_MOST,表示控件的尺寸为wrap_content if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { //宽高都为wrap_content,所以都用默认值 resultWidth = DEFAULT_WIDTH; resultHeight = DEFAULT_HEIGHT; } else if (widthMode == MeasureSpec.AT_MOST) { //宽为wrap_content,只有宽为默认的尺寸 resultWidth = DEFAULT_WIDTH; resultHeight = heightSize; } else if (heightMode == MeasureSpec.AT_MOST) { //高为wrap_content,只有高为默认的尺寸! resultWidth = widthSize; resultHeight = DEFAULT_HEIGHT; } setMeasuredDimension(resultWidth, resultHeight);}
- 运行结果如下图所示,发现灰色背景实现了wrap_content效果,与CircleView的运行结果是一致的。
2.3 处理自定义View的margin和padding
- 在上面布局文件中加上一个marginLeft属性,如下所示:
- 结果如下图,WrapContentCircleView左侧会自动添加50dp的margin值。
结论:margin是由父控件件来处理的,因此一般不需要我们再进行处理。
- 那么padding值呢?添加完padding值后发现并没有什么效果,那么需要我们自行处理了。
- 新建一个类并继承自View(这里为
PaddingCircleView
),创建构造方法,并重写onMeasure方法(同WrapContentCircleView
),更改布局文件如下:
- 重写onDraw方法,代码如下:
@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save();//保存画板 //获取当前view的所有padding int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int paddingRight = getPaddingRight(); int paddingBottom = getPaddingBottom(); int width = getWidth() - paddingLeft - paddingRight; int height = getHeight() - paddingTop - paddingBottom; int radius = Math.min(width, height) / 2;//设置圆的半径为宽和高中较小值的1/2,否则显示不全 //圆心x需要加上左边的padding,y要加上上边的padding canvas.drawCircle(radius + paddingLeft, radius + paddingTop, radius, paint);//x,y都为半径,避免因宽高不一致而造成有margin值 canvas.restore();}
- 重写onDraw方法前后运行结果如下:
2.4 创建自定义属性
- 在values目录下创建自定义属性的xml文件,一般命名为
attrs.xml
,这里自定义了一个circle_color的属性:
<?xml version="1.0" encoding="utf-8"?>
- 新建一个类并继承自View(这里为
CustomAttributesView
),并创建构造方法和onDraw方法(同CircleView
),更改布局文件为(自定义颜色属性值为深天蓝色
):
注意:需要添加命名空间 xmlns:app="http://schemas.android.com/apk/res-auto"
<?xml version="1.0" encoding="utf-8"?>
-
CustomAttributesView
的代码如下:
public class CustomAttributesView extends View { private static final int DEFAULT_CIRCLE_COLOR = Color.RED; private Paint paint; private int paintColor = DEFAULT_CIRCLE_COLOR; public CustomAttributesView(Context context) { super(context); init(null); } public CustomAttributesView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(attrs); } public CustomAttributesView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(attrs); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public CustomAttributesView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(attrs); } public void init(AttributeSet attrs) { //从布局文件中解析自定义属性 if (attrs != null) { TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.CustomAttributesView); paintColor = array.getColor(R.styleable.CustomAttributesView_circle_color, DEFAULT_CIRCLE_COLOR); array.recycle(); } //创建一个抗锯齿的画笔 paint = new Paint(Paint.ANTI_ALIAS_FLAG); //设置画笔颜色为自定义颜色 paint.setColor(paintColor); } //onDraw方法同前面的CircleView代码 @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save();//保存画板 int width = getWidth(); int height = getHeight(); int radius = Math.min(width, height) / 2;//设置圆的半径为宽和高中较小值的1/2,否则显示不全 canvas.drawCircle(radius, radius, radius, paint);//x,y都为半径,避免因宽高不一致而造成有margin值 canvas.restore(); }}
- 运行结果为:
- 倘若要在代码中重新设置颜色值,则在CustomAttributesView中添加如下方法即可,在调用invalidate会是重新执行onDraw方法。
public void setCircleColor(int color) { //若传递进来的颜色值和原来的颜色不一样,才设置 if (this.paintColor != color) { this.paintColor = color; paint.setColor(paintColor); invalidate(); }}
此外,Android还提供了postInvalidate,它们都是用来刷新界面的,区别为:invalidate是在UI线程调用,postInvalidate能在子线程和主线程中调用,但是invalidate方法效率更高。
- 然后在对应的Activity中通过
View.setCircleColor(int color)
来重新设置颜色值。
3 移动View
3.1 滚动View中的内容
我们要滚动View中的内容,就要借助scrollTo和scrollBy来实现,scrollTo滚动的是绝对坐标,即滚动到指定的位置,scrollBy滚动的是相对坐标,即在上一次滚动的基础上叠加,并且scroll方法的坐标数值是:向右滚动为负值,向下滚动为负值,和View的坐标相反。
- 要实现的效果如下图:
分析:我们要滚动TextView,则应该在TextView的外层容器上调用scroll方法。
- 新建一个Activity命名为ScrollViewActivity,其布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
- ScrollViewActivity代码如下:
public class ScrollViewActivity extends BaseActivity { @BindView(R.id.text_view_container) LinearLayout tvContainer; @Override public int getLayoutId() { return R.layout.activity_scroll_view; } @OnClick({R.id.scroll_to, R.id.scroll_by, R.id.reset}) void submit(View view) { switch (view.getId()) { case R.id.scroll_to: tvContainer.scrollTo(-100, -100); break; case R.id.scroll_by: tvContainer.scrollBy(-5, -10); break; case R.id.reset: tvContainer.scrollTo(0, 0); break; } }}
3.2 借助Scroller来滚动View
上面的滚动都是瞬间完成的,若要慢慢滚动则要借助Scroller类来实现。
- 新建一个类为ScrollContentView并继承自LinearLayout,代码如下:
public class ScrollContentView extends LinearLayout { private Scroller scroller; public ScrollContentView(Context context) { super(context); init(context); } public ScrollContentView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context); } public ScrollContentView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public ScrollContentView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context); } private void init(Context context) { //指定一个线性插值器 scroller = new Scroller(context, new LinearInterpolator()); } public void startContentScroll() { //在3秒钟内从当前位置滚动到(-300,-300)位置,若不指定时间则默认为250毫秒,单位为px scroller.startScroll(0, 0, -300, -300, 3000); invalidate();//调用该方法让View重新绘制 } //每次调用完startScroll方法后就会 @Override public void computeScroll() { super.computeScroll(); if (scroller.computeScrollOffset()) {//若还在滚动,则返回true scrollTo(scroller.getCurrX(), scroller.getCurrY()); //在调用View进行绘制,使得View的computeScroll继续执行,直至完成滚动 invalidate(); } }}
- 新建一个Activity,命名为UseScrollerActivity,其布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
- 在点击按钮后借助ScrollContentView.startContentScroll方法即可开始滚动(具体效果取决于插值器)。
3.3 移动View
要移动View,则要借助View.offsetLeftAndRight和View.offsetTopAndBottom方法了,它们会改变当前View的位置,即getX/getY、getLeft、getRight、getTop、getBottom的值。
坐标数值是向右移动View则为正数,向左移动则为负数;向下移动为正数,向上移动为负数。请仿照3.1案例自行编写demo。
3.4 使用VelocityTracker计算滑动速度
- 实现思路:
- 初始化一个VelocityTracker实例;
- 将所有的Event添加到VelocityTracker;
- 在
ACTION_MOVE
动作下调用计算方法并获取速度;(在实际应用中一般是在ACTION_UP
动作下调用计算方法并获取速度的) - 释放资源;
- 代码实现:
- 创建一个名为的Activity,代码如下:
public class ComputeVelocityActivity extends BaseActivity { private static final String TAG = "ComputeVelocityActivity"; private VelocityTracker velocityTracker; @Override pblic int getLayoutId() { return R.layout.activity_copute_velocity; } @Override public boolean onTouchEvent(MotionEvent event) { //2.将所有的Event添加到VelocityTracker addEventToVelocityTracker(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: //3.调用计算方法 velocityTracker.computeCurrentVelocity(1000, 200); //4.获取x和y方向上的速度 float xVelocity = velocityTracker.getXVelocity(); float yVelocity = velocityTracker.getYVelocity(); Log.d(TAG, "onTouchEvent: " + xVelocity + "," + yVelocity); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: releaseVelocityTracker(); break; } return super.onTouchEvent(event); } /** * 添加事件到VelocityTracker * * @param event */ private void addEventToVelocityTracker(MotionEvent event) { //1.若VelocityTracker实例为空,则初始化一个VelocityTracker实例 if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); } velocityTracker.addMovement(event); } /** * 释放Velocity对象 */ private void releaseVelocityTracker() { //5.释放资源 velocityTracker.clear(); velocityTracker.recycle(); velocityTracker = null; }}
- 布局文件为空,则在界面上下左右滑动,查看日志:
- 补充:还有一个与滑动相关的变量TouchSlop,它是系统所能识别出的最小滑动距离,若小于该距离,则系统不认为是在滑动,这与设备有关如下图所示8.0的源码中对该常量数值的定义:
获取方法:通过ViewConfiguration.get(this).getScaledTouchSlop()
来获取
ViewConfiguration这个类主要定义了UI中所使用到的标准常量,像超时、尺寸、距离等。
3.5 借助Scroller来滚动View
我们在使用scrollTo或scrollBy时,都是在瞬间完成移动,若要慢慢滚动则要借助Scroller类来实现。
- 新建一个类为ScrollContentView并继承自LinearLayout,代码如下:
public class ScrollContentView extends LinearLayout { private Scroller scroller; public ScrollContentView(Context context) { super(context); init(context); } public ScrollContentView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context); } public ScrollContentView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public ScrollContentView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context); } private void init(Context context) { //指定一个线性插值器 scroller = new Scroller(context, new LinearInterpolator()); } public void startContentScroll() { //在3秒钟内从当前位置滚动到(-300,-300)位置,若不指定时间则默认为250毫秒,单位为px scroller.startScroll(0, 0, -300, -300, 3000); invalidate();//调用该方法让View重新绘制 } //每次调用完startScroll方法后就会 @Override public void computeScroll() { super.computeScroll(); if (scroller.computeScrollOffset()) {//若还在滚动,则返回true scrollTo(scroller.getCurrX(), scroller.getCurrY()); //在调用View进行绘制,使得View的computeScroll继续执行,直至完成滚动 invalidate(); } }}
- 新建一个Activity,命名为UseScrollerActivity,其布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
- 在点击按钮后借助ScrollContentView.startContentScroll方法即可开始滚动(具体效果取决于插值器)。
4 自定义ViewGroup
4.1 自定义水平ScrollView
实现效果:
- 首先创建一个类(
HorizontalScrollView
)并继承自ViewGroup
,创建其构造方法并重写onLayout
方法; - 然后重写其
onMeasure
方法,为了测量出ScrollView的尺寸,我们需要首先测量出各个子View的尺寸,思路如下:
- 若没有子元素(或称子View),则HorizontalScrollView的宽高都为0;
- 若宽的测量模式为AT_MOST,则HorizontalScrollView的宽则为所有子元素的宽之和,
- 若高的测量模式为AT_MOST,则HorizontalScrollView的高为所有子元素中最高的子元素的高度;
代码实现如下:
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int widthResult = 0; int heightResult = 0; //首先必须测量所有的子元素 measureChildren(widthMeasureSpec, heightMeasureSpec); if (getChildCount() == 0) {//若没有子元素,则宽高为0 widthResult = 0; heightResult = 0; } else if (heightSpecMode == MeasureSpec.AT_MOST && widthSpecMode == MeasureSpec.AT_MOST) { //若宽高都为至多模式,那宽度就为所有元素宽度之和,高度就是子元素高度的最大值 widthResult = getTotalWidth(); heightResult = getMaxChildHeight(); } else if (widthSpecMode == MeasureSpec.AT_MOST) { //如果宽度为至多模式,那宽度就为所有元素宽度之和 widthResult = getTotalWidth(); heightResult = heightSpecSize; } else if (heightSpecMode == MeasureSpec.AT_MOST) { //如果高是至多模式,那高度就是子元素高度的最大值 widthResult = widthSpecSize; heightResult = getMaxChildHeight(); } else { //其他情况,为当前父类传递过来的尺寸 widthResult = widthSpecSize; heightResult = heightSpecSize; } setMeasuredDimension(widthResult, heightResult);}/** * 获取所有子元素的宽之和 * * @return 所有子元素的宽之和 */private int getTotalWidth() { int childCount = getChildCount(); int width = 0; for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); width += childView.getMeasuredWidth(); } return width;}/** * 获取最高子元素的高度 * * @return 最高子元素的高度 */private int getMaxChildHeight() { int childCount = getChildCount(); int maxHeight = 0; for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); if (childView.getMeasuredWidth() > maxHeight) { maxHeight = childView.getMeasuredWidth(); } } return maxHeight;}
- 重写onLayout方法,摆放各个子元素,代码如下:
@Overrideprotected void onLayout(boolean b, int i, int i1, int i2, int i3) { //这里先忽略padding int childLeft = 0; for (int j = 0; j < getChildCount(); j++) { View childView = getChildAt(j); //跳过状态为GONE的元素 if (childView.getVisibility() != View.GONE) { int childWidth = childView.getMeasuredWidth(); int childHeight = childView.getMeasuredHeight(); childView.layout(childLeft, 0, childLeft + childWidth, childHeight); //将child的宽度累加,作为下一个子元素起始位置 childLeft += childWidth; } }}
- 我们将布局文件放入到布局文件中(这里我们将自定义ViewGroup放在最外层),如下所示,并放入几个文本控件:
<?xml version="1.0" encoding="utf-8"?>
- 此时运行,发现并不能左右滑动,这时因为我们还需要自己处理内容的滑动,这里我们拦截所有的事件,重写
onInterceptTouchEvent
及onTouchEvent
方法即可。
/** * 拦截触摸事件 * @param ev * @return */@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) { return true;//为了简单,这里先返回true以便拦截所有事件}/** * 针对拦截的事件进行处理 * * @param event * @return */@Overridepublic boolean onTouchEvent(MotionEvent event) { final int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: if (getChildCount() == 0) {//若没有子元素,则不再拦截事件,返回false return false; } mLastX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: //滚动距离=上一次触摸点的x坐标-当前触摸点的x坐标:则向右滚动为负,向左为正 int distanceX = (int) (mLastX - event.getX()); //若原来的滚动距离+当前滚动距离>0,表示没有滚出左边的边界 //并且原来的滚动距离+当前滚动距离必须<所有子元素的宽度-当前屏幕的宽度 if (getScrollX() + distanceX > 0 && getScrollX() + distanceX <= getTotalWidth() - getWidth()) { scrollBy(distanceX, 0); } mLastX = (int) event.getX(); break; } return true;}
分析:
- getScrollX:意思是返回当前滑动View左边界的位置,如下图所示,初始时view在屏幕左侧的位置为起始位置,此时getScrollX=0;当向左滑动View100px的距离时,getScrollX=100;当向右滑动View100px的距离时,getScrollX=-100。
- 如下图将三个文本控件向左滚动:起初,红色子View的左侧与屏幕左侧齐平时,getScrollX=0,此时不能再向右滑动,则只能向左滚动,即distanceX>0,即getScrollX+distanceX>0;当蓝色的子View右侧与屏幕右侧齐平时,getScrollX=distanceX=getTotalWidth-getWidth,此时不能再向左滚动,即getScrollX+distanceX<=getTotalWidth-getWidth即可。
- 紧接着我们还需要实现
HorizontalScrollView
抬手后的惯性滚动效果,首先在onTouchEvent
方法中将event添加到VelocityTracker
中,然后在ACTION_UP
中计算滑动速度,若速度大于系统定义的最小滑动速度,则向前滑动,而滑动时借助scroller.fling()
方法实现滚动,同时重写computeScroll
方法实现,最后再释放VelocityTracker
。
新增代码如下:
public class HorizontalScrollView extends ViewGroup { private int mLastX; private VelocityTracker velocityTracker; private int mMinFlingVelocity; private int mMaxFlingVelocity; private Scroller scroller; public HorizontalScrollView(Context context) { super(context); init(); } public HorizontalScrollView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(); } private void init() { ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext()); mMinFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity(); mMaxFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity(); //可以添加一个插值器 scroller = new Scroller(getContext(),new FastOutLinearInInterpolator()); } /** * 针对拦截的事件进行处理 * * @param event * @return */ @Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getAction(); addEventToVelocityTracker(event); switch (action) { //... case MotionEvent.ACTION_UP: velocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); int xVelocity = (int) velocityTracker.getXVelocity(); //若速度大于最小滑动速度,就调用fling方法,最小滑动速度可以通过ViewConfiguration获取 if (Math.abs(xVelocity) > mMinFlingVelocity) { fling(-xVelocity);//这里记住要取反 } releaseVelocityTracker(); break; } return true; } private void addEventToVelocityTracker(MotionEvent event) { if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); } velocityTracker.addMovement(event); } private void fling(int velocity) {//参数为x方向上的速度 if (getChildCount() > 0) { int width = getWidth(); int right = getTotalWidth() - getWidth();//最大滚动距离 scroller.fling(getScrollX(), getScrollY(), velocity, 0, 0, right, 0, 0); invalidate(); } } @Override public void computeScroll() { super.computeScroll(); if (scroller.computeScrollOffset()) { scrollTo(scroller.getCurrX(), scroller.getCurrY()); invalidate(); } } private void releaseVelocityTracker() { velocityTracker.clear(); velocityTracker.recycle(); velocityTracker = null; }}
5 MotionEvent
- 常用事件:
-
ACTION_DOWN
:第一个触摸点按下; -
ACTION_UP
:最后一个触摸点抬起; -
ACTION_MOVE
:当触摸点在屏幕上移动则会触发该事件; -
ACTION_CANCEL
:用户不能触发,是由系统触发的,比如:当父容器通过onInterceptTouchEvent
方法返回true,那么子View就会收到一个ACTION_CANCEL
事件,后面就不会再有事件传递给他; -
ACTION_OUTSIDE
:用户触摸超出了正常的UI边界; -
ACTION_POINTER_DOWN
:当有一个触点后,再有别触点按下; -
ACTION_POINTER_UP
:不是最后一个触点抬起; -
ACTION_SCROLL
:一般是鼠标,滚轮,轨迹球才触
- 常用方法:
-
event.getAction()
:动作类型,不能用它判断多点; -
event.getActionMasked()
:多点的动作类型,不管单点或者多点都可以用它; -
event.getActionIndex()
:当前MotionEvent是第几点触控; -
event.getPointerCount()
:当前共有多少个触摸点; -
event.getX()/event.getY()
:相对于当前View左上角的X、Y坐标,可以说是这个View内的坐标。; -
event.getRawX()/event.getRawy()
:相对于手机屏幕的左上角的X、Y坐标,包括手机的状态栏高度。 -
event.getX(pointIndex)/event.getY(pointIndex)
:获取对应的触摸点坐标; -
event.getDownTime()/event.getEventTime()
:按下或抬起时间;
-
MotionEvent
是一个32位的int值,低16位代表触控的动作(getActionMasked
),高16位代表触控点的索引(getActionIndex
)。
这点和 MeasureSpec
很像,高2位代表SpecMode,低30位代表了SpecSize,即测量模式和测量尺寸。
案例代码下载地址:https://github.com/crazywish/...
参考资料:
- Android getScrollX()详解
- 爱学啊之《详解View》
更多相关文章
- android之日志打印管理封装类
- Android(安卓)自定义View探索——图片
- 2、从头学Android之第一个Activity程序
- Android(安卓)自定义流式布局
- android studio升级失败提示 Connection failed解决方法
- Android:Service:服务的生命周期
- Android——本地服务基础(一)
- 关于Android中Service的onDestory()调用时机
- Android实现TCP客户端接收数据的方法