自定义控件android自定义view

Android Scroller分析

2019-04-26  本文已影响44人  NoBugException
(1)Android view的直角坐标系
图片.png

需要注意的是,view的直角坐标系和数学的直角坐标系不同,view的x轴方向和数学的直接坐标系一致,但是y轴方向却相反。

(2)基本方法

在介绍Scroller之前,我们先来看一下view移动的基础方法。

获取x轴方向和y轴方向偏移量,如view直角坐标系所示,view的左上角在原点处,也就是说,初始状态下 getScrollX()getScrollY()的值为0,往左移动时偏移量x为负数,往右移动时偏移量x为正数,往上移动时偏移量y为负数,往下移动时偏移量y为正数。

view的内容移动到指定位置。

这里需要注意的是,不是移动view,而是移动view的内容。

这个方法往往结合onTouchEvent一起使用,我们看以下代码

public class TestView extends View {

    private float mLastX = 0;
    private float mLastY = 0;

    private Paint mPaint;

    public TestView(Context context) {
        super(context);
        init();
    }

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init(){
        mPaint = new Paint();
        mPaint.setTextSize(80);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawText("我是中国人!", 0, 100, mPaint);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = event.getX();
        float y = event.getY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //标志着第一个手指按下
                mLastX = x;//获取按下时x坐标值
                mLastY = y;//获取按下时y坐标值
                break;
            case MotionEvent.ACTION_MOVE:
                //按住一点手指开始移动
                float move_x = mLastX - x;//计算当前已经移动的x轴方向的距离
                float move_y = mLastY - y;//计算当前已经移动的y轴方向的距离
                float oldScollX = getScrollX();//计算之前已经偏移的x轴方向的距离
                float oldScollY  = getScrollY();//计算之前已经偏移的y轴方向的距离

                scrollTo((int) move_x, (int) move_y);

                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //表示手势被取消了,不再接受后续事件
                scrollTo(0, 0);
                break;

        }
        return true;
    }
}

我们自定义一个view,view仅仅绘制一个文本,这个文本就是view的内容,代码的逻辑是:移动view,在移动的过程中view的内容也随之移动,当结束移动时,view的内容位置恢复。

图片效果:

46.gif

我们先来看一下scrollTo,假如我们去掉触摸时间的处理

public class TestView extends View {

    private Paint mPaint;

    public TestView(Context context) {
        super(context);
        init();
    }

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init(){
        mPaint = new Paint();
        mPaint.setTextSize(80);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawText("我是中国人!", 0, 100, mPaint);

    }
}

这样的话文本的位置是:


图片.png

现在我们使用代码控制view内容的位置

    tv_text = findViewById(R.id.tv_text);
    tv_text.scrollTo(-100, -100);

移动之后的效果如下:

图片.png

当我们多次使用scrollTo

    tv_text = findViewById(R.id.tv_text);
    tv_text.scrollTo(-100, -100);
    tv_text.scrollTo(-100, -100);
    tv_text.scrollTo(-100, -100);
    tv_text.scrollTo(-100, -100);
    tv_text.scrollTo(-100, -100);

效果如下:

图片.png

我们发现不管我们使用多少次scrollTo,view的移动都是以最开始的位置开始的。

scrollBy(x, y)可以完美解决这个问题,我们看一下源码就知道它的具体作用了:

/**
 * Move the scrolled position of your view. This will cause a call to
 * {@link #onScrollChanged(int, int, int, int)} and the view will be
 * invalidated.
 * @param x the amount of pixels to scroll by horizontally
 * @param y the amount of pixels to scroll by vertically
 */
public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

scrollBy(x, y)使每次移动都是以当前位置开始。

    tv_text = findViewById(R.id.tv_text);
    tv_text.scrollBy(-100, -100);
    tv_text.scrollBy(-100, -100);
    tv_text.scrollBy(-100, -100);
    tv_text.scrollBy(-100, -100);
    tv_text.scrollBy(-100, -100);
    tv_text.scrollBy(-100, -100);
    tv_text.scrollBy(-100, -100);

效果如下:

图片.png
(3)Scroller滑动辅助类的基本方法

Scroller本身不会去移动view,它只是一个移动计算辅助类,用于跟踪控件滑动的轨迹,只相当于一个滚动轨迹记录工具,最终还是通过View的scrollTo、scrollBy方法完成View的移动的。

获取mScroller当前水平滚动的位置

获取mScroller当前竖直滚动的位置

获取mScroller最终停止的水平位置

获取mScroller最终停止的竖直位置

开始滚动动画:
startX:滚动的x方向起始点
startY:滚动的y方向起始点
dx:x方向的偏移量
dy:y方向的偏移量
duration:滚动所消耗的时间,默认为250毫秒

startScroll(int startX, int startY, int dx, int dy)
startScroll(int startX, int startY, int dx, int dy, int duration)

判断滚动动画是否结束:
true:滚动尚未完成
false:滚动已经完成

(4)基本代码实现
public class TestView extends View {

    private float mDownX = 0;
    private float mDonwY = 0;
    private Paint mPaint;

    private Scroller mScroller;

    public TestView(Context context) {
        super(context);
        init(context);
    }

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context mContext){
        mPaint = new Paint();
        mPaint.setTextSize(80);

        mScroller = new Scroller(mContext);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawText("我是中国人!", 0, 100, mPaint);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //标志着第一个手指按下
                mDownX = x;//获取按下时x坐标值
                mDonwY = y;//获取按下时y坐标值
                break;
            case MotionEvent.ACTION_MOVE:
                //按住一点手指开始移动
                float move_x = mDownX - x;//计算当前已经移动的x轴方向的距离
                float move_y = mDonwY - y;//计算当前已经移动的y轴方向的距离
                float oldScollX = getScrollX();//计算之前已经偏移的x轴方向的距离
                float oldScollY  = getScrollY();//计算之前已经偏移的y轴方向的距离

                //开始滚动动画
                //第一个参数:x轴开始位置
                //第二个参数:y轴开始位置
                //第三个参数:x轴偏移量
                //第四个参数:y轴偏移量
                mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), (int) move_x, (int) move_y, 3000);

                invalidate();//目的是重绘view,是的执行computeScroll方法

                break;

        }
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){//判断滚动是否完成,true说明滚动尚未完成,false说明滚动已经完成
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());//将view直接移动到当前滚动的位置
            invalidate();//触发view重绘
        }
    }
}

效果如下:

47.gif

看到这三秒钟的滚动动画了吧,默认情况下就是这个效果,默认情况下Scroller使用的插值器是ViscousFluidInterpolator,从字面意义上看是一个粘性流体插值器。

(5)构造方法
//默认插值器是ViscousFluidInterpolator
Scroller mScroller = new Scroller(mContext);

//指定一个插值器
Scroller mScroller = new Scroller(mContext, new AccelerateDecelerateInterpolator());

//指定一个插值器,第三个参数表示是否开启“飞轮”效果,也就是多次滚动时速度叠加
Scroller mScroller = new Scroller(mContext, new AccelerateDecelerateInterpolator(), false);
(6)插值器
图片.png

Scroller其实就是在scrollTo(x, y)scrollBy(x, y)的基础上添加滚动效果,滚动效果是一个动画,当我们new一个Scroller对象时,就已经指定了一个插值器,下面来说明一下各种插值器:

这是一个默认插值器,当构造Scroller时,如果不传递插值器或者插值器为null时,系统默认使用ViscousFluidInterpolator插值器。

在动画开始与结束的时候速率改变比较慢,在中间的时候速率较快。

在动画开始的地方速率改变比较慢,然后开始加速。

开始的时候向后然后向前甩。

如图所示

48.gif

开始的时候向后然后向前甩一定值后返回最后的值。

如图所示:

49.gif

反弹插值器。

如图所示:


50.gif

动画循环播放特定的次数,速率改变沿着正弦曲线。

51.gif

在动画开始的地方快然后慢。

52.gif

以常量速率改变。

52.gif

向前甩一定值后再回到原来位置.

路径插值器,我们可以按照自己想要的轨迹滚动。

PathInterpolator(Path path)
PathInterpolator(float controlX, float controlY)
PathInterpolator(float controlX1, float controlY1, float controlX2, float controlY2)

如果学习Path使用的话,这篇博客是个不错的选择Android开发之Path详解

MaterialDesign基于贝塞尔曲线的插补器效果:依次慢慢快。

基于贝塞尔曲线的插补器效果:依次慢快慢

基于贝塞尔曲线的插补器效果:依次快慢慢

以上的插值器运用比较广泛,在Scroller中设置一个插值器可以优化滚动的效果。

上一篇下一篇

猜你喜欢

热点阅读