自定义View-平滑滚动

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_32113133/article/details/65634862

TouchSlidingScreen:

public class TouchSlidingScreen extends ViewGroup {
    //平滑滚动中要用到Scroller
    private Scroller scroller;
    //最小滑动距离,超过了才认为开始滑动
    private int touchSlop = 0;
    //停止状态
    private static final int TOUCH_STATE_STOP = 0;
    //滑动状态
    private static final int TOUCH_STATE_FLING = 1;
    private int touchState = TOUCH_STATE_STOP;
    ///上次触摸屏的 x 位置
    private float lastX = 0;
    //当前位置
    private int curScreen;
    //VelocityTracker 主要用于跟踪触摸屏事件(flinging 事件和其他 gestures 手势事件)的速率
    private VelocityTracker velocityTracker;
    public static int SNAP_VELOCITY = 600;  //最小的滑动速率

    public TouchSlidingScreen(Context context) {
        this(context, null);
    }

    public TouchSlidingScreen(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TouchSlidingScreen(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init(context);
    }

    public void init(Context context) {
        scroller = new Scroller(context);
        //获取到当前手机上默认的最小滑动距离
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //测量子View
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int width = measureWidth(widthMeasureSpec);
        int height = measureHeight(heightMeasureSpec);

        //保存测量自己的宽高
        setMeasuredDimension(width, height);
    }

    //子View宽必须是match_parent,容器总的宽是一屏*子View的个数
    private int measureWidth(int widthMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int width = 0;
        //ViewGroup不能是包裹
        if (widthMode == MeasureSpec.AT_MOST) {
            throw new IllegalStateException("the width must is match_parent!");
        } else {
            width = widthSize;
        }
        return width * getChildCount();
    }

    //容易的高必须是match_parent
    private int measureHeight(int heightMeasureSpec) {
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int height = 0;
        if (heightMode == MeasureSpec.AT_MOST) {
            throw new IllegalStateException("the height must is match_parent!");
        } else {
            height = heightSize;
        }
        return height;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        int childWidth = (r - l) / count;//一屏的宽度
        int childHeight = b - t;//一屏的高度

        for (int i = 0; i < count; i++) {
            int left = i * childWidth;
            int top = 0;
            int right = (i + 1) * childWidth;
            int bottom = childHeight;

            View childView = getChildAt(i);
            //定位子View
            childView.layout(left, top, right, bottom);
        }
    }

    //事件拦截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        if (action == MotionEvent.ACTION_MOVE && touchState == TOUCH_STATE_STOP) {//滑动并且屏幕中的状态是停止时
            return true;
        }
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                touchState = scroller.isFinished() ? TOUCH_STATE_STOP : TOUCH_STATE_FLING;
                break;
            case MotionEvent.ACTION_HOVER_MOVE:
                //滑动距离过小不算滑动
                int dx = (int) Math.abs(x - lastX);
                if (dx > touchSlop) {
                    touchState = TOUCH_STATE_FLING;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                touchState = TOUCH_STATE_STOP;
                break;
        }
        return touchState != TOUCH_STATE_STOP;//滑动时返回true拦截,不交给子View
    }

    /**
     * @param event
     * @return
     */
    public boolean onTouchEvent(MotionEvent event) {
        if (velocityTracker == null) {
            //使用 obtain()方法得到这个类的实例
            velocityTracker = VelocityTracker.obtain();
        }
        //addMovement(MotionEvent)函数将你接受到的 motion	event 加入到 VelocityTracker 类实例中
        velocityTracker.addMovement(event);
        super.onTouchEvent(event);
        int action = event.getAction();
        final int x = (int) event.getX();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //手指按下时,如果正在滚动,则立刻停止
                if (scroller != null && !scroller.isFinished()) {
                    scroller.abortAnimation();
                }
                lastX = x;
                break;
            case MotionEvent.ACTION_MOVE:
                //随手指滚动
                int dx = (int) (lastX - x);
                scrollBy(dx, 0);
                lastX = x;
                break;
            case MotionEvent.ACTION_UP:
                final VelocityTracker velocityTracker = this.velocityTracker;
                //使用 computeCurrentVelocity(int)初始化速率的单位
                velocityTracker.computeCurrentVelocity(1000);
                //获得x方向的速率
                int velocityX = (int) velocityTracker.getXVelocity();
                //通过 velocityX 的正负值可以判断滑动方向
                if (velocityX > SNAP_VELOCITY && curScreen > 0) {
                    moveToPrevious();
                } else if (velocityX < -SNAP_VELOCITY && curScreen < (getChildCount() - 1)) {
                    moveToNext();
                } else {
                    moveToDestination();
                }
                if (velocityTracker != null) {//释放并回收资源
                    this.velocityTracker.clear();
                    this.velocityTracker.recycle();
                    this.velocityTracker = null;
                }
                touchState = TOUCH_STATE_STOP;
                break;
            case MotionEvent.ACTION_CANCEL:
                touchState = TOUCH_STATE_STOP;
                break;
        }
        return true;
    }

    public void moveToScreen(int whichScreen) {
        curScreen = whichScreen;
        int scrollX = getScrollX();
        int splitWidth = getWidth() / getChildCount();
        int dx = curScreen * splitWidth - scrollX;
        scroller.startScroll(scrollX, 0, dx, 0, Math.abs(dx));
        invalidate();
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            postInvalidate();
        }
    }
    
    public void moveToDestination() {
        //每一屏的宽度
        int splitWidth = getWidth() / getChildCount();
        //判断是回滚还是进入下一分屏
        int toScreen = (getScrollX() + splitWidth / 2) / splitWidth;
        //移动到目标分屏
        moveToScreen(toScreen);
    }

    /**
     * 滚动到下一屏
     */
    public void moveToNext() {
        if (curScreen >= getChildCount() - 1) {
            Toast.makeText(getContext(), "当前已经是最后一页", Toast.LENGTH_SHORT).show();
            return;
        }
        moveToScreen(curScreen + 1);
    }

    /**
     * 滚动到上一屏
     */
    public void moveToPrevious() {
        if (curScreen <= 0) {
            Toast.makeText(getContext(), "当前已经是第一页", Toast.LENGTH_SHORT).show();
            return;
        }
        moveToScreen(curScreen - 1);
    }

}

activity_main:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.example.user.myapplication4.TouchSlidingScreen
        android:id="@+id/ml"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="#FFFF00"
            android:orientation="vertical"></LinearLayout>

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="#00FF00"
            android:orientation="vertical"></LinearLayout>

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="#0000FF"
            android:orientation="vertical"></LinearLayout>

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="#00FFFF"
            android:orientation="vertical"></LinearLayout>
    </com.example.user.myapplication4.TouchSlidingScreen>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/pre"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="上一屏" />

        <Button
            android:id="@+id/next"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="@android:color/holo_green_light"
            android:text="下一屏" />
    </LinearLayout>

</LinearLayout>

MainActivity:

public class MainActivity extends Activity {
    private TouchSlidingScreen ml;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ml = (TouchSlidingScreen) findViewById(R.id.ml);
        Button pre = (Button) findViewById(R.id.pre);
        Button next = (Button) findViewById(R.id.next);

        pre.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ml.moveToPrevious();
            }
        });
        next.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ml.moveToNext();
            }
        });

        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
               handler.sendEmptyMessage(0);
            }
        },2000,3000);
    }

    Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case 0:
                    ml.moveToNext();
                    break;
            }
        }
    };
}


第一步:初始化。平滑滚动需要使用 Scroller 对象,另外还需要给定一个最小滑动距离,通过 ViewConfiguration.get(context).getScaledTouchSlop()可以获取到当前手机上默认的最小滑动距离。

第二步:测量容器宽度与高度。不允许使用 MeasureSpec.AT_MOST,每个子组件与容器相同,容器的 layout_width 值虽然为 MeasureSpec. EXACTLY,但容器大小 =父容器的宽度 * 子组件的个数,高度与父容器相同。

第三步:定位子组件。默认情况下,屏幕出现第一个子组件,子组件占满容器的可见区域,其他子组件以相同大小依次排列在后面。

第四步:判断滚动状态,状态为分两种:停止状态和滑动状态。容器根据状态决定是否截拦事件。

第五步:惯性滚屏:

手指滑动距离如果超过容器一半或者滑动速度足够快,则进入下一屏(或者上一屏)。如果没有超过一半或速度很慢则回滚到初始位置。定义 moveToDestination()方法如下,最关键的语句是 int toScreen= (getScrollX()+ splitWidth/ 2 ) / splitWidth,getScrollX()是容器滚动过的距离,splitWidth 是每一屏的宽度。比如每一屏的宽度为 10,当前屏为第 2 屏,容器已滚过 23,则 toScreen= (23 +10 / 2) / 10= (23 + 5) / 10 = 28/ 10 = 2.8 = 2,也就是说要回滚到第 2 屏;如果容器已滚动28,则 toScreen =(28 + 10 / 2)/ 10 = 32 /10 = 3.2 = 3,表示要滚动到第 3 屏。

第六步:响应用户手指的按下、移动和松开事件,这是整个滑动的关键,特别是松开后,要判断滚屏还是回滚。为了支持上一屏和下一屏,需要辨别手指滑动的方向,VelocityTracker 类可以获取 x 方向的速率,其正值代表向左滑动,负值代表向右滑动。如果 x 方向的速率在[-SNAP_VELOCITY,SNAP_VELOCITY]之间,则要根据用户滑动的距离(滑动距离是否超过一屏的1/2)决定是要继续滚屏还是回滚到初始状态。

VelocityTracker :
addMovement(MotionEvent)函数将你接受到的 motion event 加入到 VelocityTracker 类实例中。当我们需要使用到速率时,使用 computeCurrentVelocity(int)初始化速率的单位,并获得当前的事件的速率,然后使用 getXVelocity() 或 getXVelocity()获得横向和竖向的速率。另外,通过VelocityTracker 还可以知道手指的滑动方向。

VelocityTracker 的基本使用如下:
手指按下时(ACTION_DOWN),获取 VelocityTracker 对象
if(velocityTracker== null){
//创建 velocityTracker 对象
velocityTracker= VelocityTracker.obtain();
}
//关联事件对象
velocityTracker.addMovement(ev);
手指移动过程中(ACTION_MOVE),计算速率
velocityTracker.computeCurrentVelocity(1000);
获取 x、y 两个方向的速率:
int velocityX = velocityTracker.getXVelocity();
int velocityY =velocityTracker.getYVelocity();
手指松开后(ACTION_UP),释放并回收资源
//释放 VelocityTracker 资源
if(velocityTracker !=null){
velocityTracker.clear();
velocityTracker.recycle();
velocityTracker =null;
}

猜你喜欢

转载自blog.csdn.net/qq_32113133/article/details/65634862