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的工作流程

  1. View的工作流程一般涉及到2个方法:
  • onMeasure:测量View自身的尺寸
  • onDraw:根据测量的尺寸来绘制自身

  1. 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+translationXy=top+translationY

应用场景: 在属性动画中,top和left表示原始的左上角位置信息,并不会发生改变,而改变的是translationX和translationY(translationX和translationY默认值为0),因此导致x,y也发生改变。

2 实现自定义View

2.1 使用onDraw方法画一个圆

  • 先简单归纳以下思路:
  1. 新建一个类并继承自View,并创建其构造方法(这里为CircleView,并创建所有的构造方法)
  2. 在构造方法中创建一个方法init,在其中做一些初始化工作(创建画笔,设置画笔颜色等等)
  3. 重写onDraw方法。
  • 代码实现:
  1. 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();    }}
  1. 然后在布局文件中引入CircleView控件:
为了更好地理解自定义View,这里添加一个灰色的背景色( #808080
  1. 运行结果如下所示:

2.2 实现wrap_content

先来了解下测量模式,在MeasureSpec类中定义了三种模式:

  1. UNSPECIFIED:未指定,父控件对子控件没有任何约束,想要多大就多大,比如:ScrollView。
  2. EXACTLY:确切的值,表示父控件为子控件指定的确定的大小,他对应于LayoutParams中的match_parent和具体的数值。
  3. AT_MOST:至多,一般是父控件为子控件指定了最大值,子控件不要超过他,一般是子控件使用了wrap_content。
  1. 新建一个类并继承自View(这里为WrapContentCircleView),并创建构造方法和onDraw方法(同CircleView),更改布局文件为:
  1. 在将上面布局文件中的WrapContentCircleView的layout_width属性改为wrap_content后,发现CircleView的宽仍然填充了父布局,如下图灰色背景。

  1. 那么如何实现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);}
  1. 运行结果如下图所示,发现灰色背景实现了wrap_content效果,与CircleView的运行结果是一致的。

2.3 处理自定义View的margin和padding

  1. 在上面布局文件中加上一个marginLeft属性,如下所示:
  1. 结果如下图,WrapContentCircleView左侧会自动添加50dp的margin值。

结论:margin是由父控件件来处理的,因此一般不需要我们再进行处理。
  1. 那么padding值呢?添加完padding值后发现并没有什么效果,那么需要我们自行处理了。
  2. 新建一个类并继承自View(这里为PaddingCircleView),创建构造方法,并重写onMeasure方法(同WrapContentCircleView),更改布局文件如下:
  1. 重写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();}
  1. 重写onDraw方法前后运行结果如下:

2.4 创建自定义属性

  1. 在values目录下创建自定义属性的xml文件,一般命名为attrs.xml,这里自定义了一个circle_color的属性:
<?xml version="1.0" encoding="utf-8"?>                
  1. 新建一个类并继承自View(这里为CustomAttributesView),并创建构造方法和onDraw方法(同CircleView),更改布局文件为(自定义颜色属性值为深天蓝色):
注意:需要添加命名空间 xmlns:app="http://schemas.android.com/apk/res-auto"
<?xml version="1.0" encoding="utf-8"?>    
  1. 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();    }}
  1. 运行结果为:

  1. 倘若要在代码中重新设置颜色值,则在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方法效率更高。
  1. 然后在对应的Activity中通过View.setCircleColor(int color)来重新设置颜色值。

3 移动View

3.1 滚动View中的内容

我们要滚动View中的内容,就要借助scrollTo和scrollBy来实现,scrollTo滚动的是绝对坐标,即滚动到指定的位置,scrollBy滚动的是相对坐标,即在上一次滚动的基础上叠加,并且scroll方法的坐标数值是:向右滚动为负值,向下滚动为负值,和View的坐标相反。

  1. 要实现的效果如下图:


分析:我们要滚动TextView,则应该在TextView的外层容器上调用scroll方法。

  1. 新建一个Activity命名为ScrollViewActivity,其布局文件如下:
<?xml version="1.0" encoding="utf-8"?>                            
  1. 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类来实现。

  1. 新建一个类为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();        }    }}
  1. 新建一个Activity,命名为UseScrollerActivity,其布局文件如下:
<?xml version="1.0" encoding="utf-8"?>                    
  1. 在点击按钮后借助ScrollContentView.startContentScroll方法即可开始滚动(具体效果取决于插值器)。

3.3 移动View

要移动View,则要借助View.offsetLeftAndRight和View.offsetTopAndBottom方法了,它们会改变当前View的位置,即getX/getY、getLeft、getRight、getTop、getBottom的值。
坐标数值是向右移动View则为正数,向左移动则为负数;向下移动为正数,向上移动为负数。请仿照3.1案例自行编写demo。

3.4 使用VelocityTracker计算滑动速度

  • 实现思路:
  1. 初始化一个VelocityTracker实例;
  2. 将所有的Event添加到VelocityTracker;
  3. ACTION_MOVE动作下调用计算方法并获取速度;(在实际应用中一般是在ACTION_UP动作下调用计算方法并获取速度的)
  4. 释放资源;
  • 代码实现:
  1. 创建一个名为的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;    }}
  1. 布局文件为空,则在界面上下左右滑动,查看日志:

  • 补充:还有一个与滑动相关的变量TouchSlop,它是系统所能识别出的最小滑动距离,若小于该距离,则系统不认为是在滑动,这与设备有关如下图所示8.0的源码中对该常量数值的定义:


获取方法:通过ViewConfiguration.get(this).getScaledTouchSlop()来获取

ViewConfiguration这个类主要定义了UI中所使用到的标准常量,像超时、尺寸、距离等。

3.5 借助Scroller来滚动View

我们在使用scrollTo或scrollBy时,都是在瞬间完成移动,若要慢慢滚动则要借助Scroller类来实现。

  1. 新建一个类为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();        }    }}
  1. 新建一个Activity,命名为UseScrollerActivity,其布局文件如下:
<?xml version="1.0" encoding="utf-8"?>                    
  1. 在点击按钮后借助ScrollContentView.startContentScroll方法即可开始滚动(具体效果取决于插值器)。

4 自定义ViewGroup

4.1 自定义水平ScrollView

实现效果:

  1. 首先创建一个类(HorizontalScrollView)并继承自ViewGroup,创建其构造方法并重写onLayout方法;
  2. 然后重写其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;}
  1. 重写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;        }    }}
  1. 我们将布局文件放入到布局文件中(这里我们将自定义ViewGroup放在最外层),如下所示,并放入几个文本控件:
<?xml version="1.0" encoding="utf-8"?>            
  1. 此时运行,发现并不能左右滑动,这时因为我们还需要自己处理内容的滑动,这里我们拦截所有的事件,重写onInterceptTouchEventonTouchEvent方法即可。
/**  * 拦截触摸事件  * @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即可。


  1. 紧接着我们还需要实现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

  1. 常用事件:
  • ACTION_DOWN第一个触摸点按下;
  • ACTION_UP最后一个触摸点抬起;
  • ACTION_MOVE:当触摸点在屏幕上移动则会触发该事件;
  • ACTION_CANCEL:用户不能触发,是由系统触发的,比如:当父容器通过onInterceptTouchEvent方法返回true,那么子View就会收到一个ACTION_CANCEL事件,后面就不会再有事件传递给他;
  • ACTION_OUTSIDE:用户触摸超出了正常的UI边界;
  • ACTION_POINTER_DOWN:当有一个触点后,再有别触点按下;
  • ACTION_POINTER_UP:不是最后一个触点抬起;
  • ACTION_SCROLL:一般是鼠标,滚轮,轨迹球才触
  1. 常用方法:
  • 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():按下或抬起时间;
  1. MotionEvent是一个32位的int值,低16位代表触控的动作(getActionMasked),高16位代表触控点的索引(getActionIndex)。
这点和 MeasureSpec很像,高2位代表SpecMode,低30位代表了SpecSize,即测量模式和测量尺寸。

案例代码下载地址:https://github.com/crazywish/...


参考资料:

  1. Android getScrollX()详解
  2. 爱学啊之《详解View》

更多相关文章

  1. android之日志打印管理封装类
  2. Android(安卓)自定义View探索——图片
  3. 2、从头学Android之第一个Activity程序
  4. Android(安卓)自定义流式布局
  5. android studio升级失败提示 Connection failed解决方法
  6. Android:Service:服务的生命周期
  7. Android——本地服务基础(一)
  8. 关于Android中Service的onDestory()调用时机
  9. Android实现TCP客户端接收数据的方法

随机推荐

  1. android4.2视频通讯应用源码共享
  2. 获取Android系统时间是24小时制还是12小
  3. Android 性能优化 之谈谈Java内存区域
  4. Android热修复技术原理
  5. android framework层 学习笔记(二)
  6. Android——在SurfaceView上绘图
  7. Android第二周(第二部分)-listview
  8. 【开源框架】一个基于回调机制的多线程异
  9. html5游戏移植到android并打包成apk,加广
  10. 弹幕刷屏之术——Android无时间线弹幕实