Android 自定义滑动容器View

目标:自定义一个可以滑动的容器view,里面可以放多个子view,提供滑动

滑动原理:View 本来就提供 滑动的方法 scrollTo,也就是说 其实所有继承与 View 的 都可以 “滑动”!,至于原生提供的 一些View 无法滑动 (比如 TextView),原因只是它没有去处理 相关的触摸事件而已,对此,我们要 自定义一个 滑动view,可以通过去处理touch事件即可在处理 touch事件的时候,分离出 MOVE 动作,在 滑动的时候 在Move动作中 计算出 与上次move动作的 相距距离,然后通过调用 View.scrollTo 来滑动。

首先 我们 继承 ViewGroup,因为 我们是个”容器”,

public class MyScrollerView extends ViewGroup 

由于我们的继承是viewgroup (当然你可以去继承其他的layout,比如LinearLayout)所以需要 去实现他的onlayout方法,并去重写他的onMeasure 方法,
下面我们实现是一个垂直方向滚动的的自定义view,所以这里 去使用垂直布局方式去写layout,和Measure。
但是有一点要注意的是: onMeasure的时候,由于我们这个 view容器 有点类似 原生的ScrollView ,但是我们允许MyScrollView 支持多个子View,也就是说,这个MyScrollView 的高度是固定的,只允许他的子View在它内部滑动,所以在 childView measure的时候让他忽略高度的去meause(wrap_content) 去计算他的高度


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            //child 的 height 必须是 wrapcontent,由于此view允许滑动,所以子view都要计算出来
            child.measure(widthMeasureSpec, 0);
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //垂直布局
        int layoutLeft = 0;
        int layoutTop = 0;
        int layoutRight = r;
        int layoutBottom = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            layoutBottom = layoutTop + child.getMeasuredHeight();
            child.layout(layoutLeft, layoutTop, layoutRight, layoutBottom);
            layoutTop = layoutBottom;
        }
    }

好了上面是布局,下面开始处理 touch事件,由于我们的这个是个容器,需要处理touch事件,而不去拦截处理这个touch事件,所以我们需要在 dispatchTouchEvent 方法中处理

    int[] mLastTouchLocation = new int[2];

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        //手工滑动
        final int action = event.getAction();

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

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastTouchLocation[0] = x;
                mLastTouchLocation[1] = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int sx = mLastTouchLocation[0];
                int sy = mLastTouchLocation[1];
                //计算出移动的距离
                int dx = x - sx;
                int dy = y - sy;
                //记下最后这个点的位置
                mLastTouchLocation[0] = x;
                mLastTouchLocation[1] = y;

                //计算出目标的x,y 坐标点,由于View要求滑动的方向,应该与手指滑动的方向一致,所以这里 应该取反,可以想象下 在你手指往下滑动的时候,你这个view的滑动坐标是需要减少的
                int tx = getScrollX() + (-dx);
                int ty = getScrollY() + (-dy);

                //TODO 检测y的边界,让他 不能滑倒底部继续滑动
                //滑动到 目标坐标
                scrollTo(0, ty);
                //必须 调用重画,不然他只是滑动,并有在界面上显示
                postInvalidate();
                break;
        }

        return super.dispatchTouchEvent(event);
    }

好了,这个view 就搞定了,这下这个view可以划了

附上完整代码

package com.zthink.scorller;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

/**
 * 滑动
 */
public class MyScrollerView extends ViewGroup {

    int[] mLastTouchLocation = new int[2];

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

    public MyScrollerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MyScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public MyScrollerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    protected void init() {

    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        //手工滑动
        final int action = event.getAction();

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

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastTouchLocation[0] = x;
                mLastTouchLocation[1] = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int sx = mLastTouchLocation[0];
                int sy = mLastTouchLocation[1];
                int dx = x - sx;
                int dy = y - sy;
                mLastTouchLocation[0] = x;
                mLastTouchLocation[1] = y;

                int tx = getScrollX() + (-dx);
                int ty = getScrollY() + (-dy);

                //TODO 检测y的边界,让他 不能滑倒底部继续滑动
                scrollTo(0, ty);
                postInvalidate();
                break;
        }

        return super.dispatchTouchEvent(event);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            child.measure(widthMeasureSpec, 0);//child 的 height 必须是 wrapcontent,由于此view允许滑动,所以子view都要计算出来
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //垂直布局
        int layoutLeft = 0;
        int layoutTop = 0;
        int layoutRight = r;
        int layoutBottom = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            layoutBottom = layoutTop + child.getMeasuredHeight();
            child.layout(layoutLeft, layoutTop, layoutRight, layoutBottom);
            layoutTop = layoutBottom;
        }
    }


}

activity

public class MainActivity extends AppCompatActivity {

    MyScrollerView mMyScrollerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mMyScrollerView = (MyScrollerView) findViewById(R.id.scoller_view);

        for (int i = 0; i < 100; i++) {
            Button tv = new Button(this);
            tv.setText(String.valueOf(i));
            tv.setBackgroundColor(Color.BLACK);
            tv.setTextColor(Color.WHITE);
            tv.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(MainActivity.this, "chilk ", Toast.LENGTH_SHORT).show();
                }
            });
            mMyScrollerView.addView(tv);
        }
    }
}

===========================分割线=============================
好了上面,实现了通过touch来控制滑动,下面我们试着添加一个平滑滑动的效果。现在我们需要增加一个 smoothScrollTo 的方法来 让这个view 在某个时间段内平滑的滑动一段距离,这个我们用到 一个辅助类

android.widget.Scroller

这个类能 帮助我们去平滑的计算出这个滑动效果的坐标,要使用这个类,可以通过以下步骤
1.初始化一个Scroller
2.在 smoothScrollTo 方法里面 调用 Scroller.startScroll 来让他启动滑动
3.重写View的 computeScroll 方法,在里面判断 Scroller 有没有滑动,如果有滑动那么就调用View.scrollTo 来真正的滑动这个view。

那么这个Scroller的原理是?这里就详细介绍了。可以通过这篇文章来 了解
传送门:Android 带你从源码的角度解析Scroller的滚动实现原理

    Scroller mScroller;
    protected void init() {
        mScroller = new Scroller(getContext());
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {//判断是否有滑动过
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();//重画
        }
        super.computeScroll();
    }

    public void smoothScrollTo(int y) {
        //TODO 检测y的边界,
        mScroller.abortAnimation();//标志上次 的滑动结束
        mScroller.startScroll(getScrollX(), getScrollY(), 0, y);//移动到xy
        postInvalidate();//让view重画
    }

完整类代码

/**
 * 滑动
 */
public class MyScrollerView extends ViewGroup {

    Scroller mScroller;
    int[] mLastTouchLocation = new int[2];

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

    public MyScrollerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MyScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public MyScrollerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    protected void init() {
        mScroller = new Scroller(getContext());
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {//判断是否有滑动过
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();//重画
        }
        super.computeScroll();
    }

    public void smoothScrollTo(int y) {
        //TODO 检测y的边界,
        mScroller.abortAnimation();//标志上次 的滑动结束
        mScroller.startScroll(getScrollX(), getScrollY(), 0, y);//移动到xy
        postInvalidate();//让view重画
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        //手工滑动
        final int action = event.getAction();

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

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastTouchLocation[0] = x;
                mLastTouchLocation[1] = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int sx = mLastTouchLocation[0];
                int sy = mLastTouchLocation[1];
                int dx = x - sx;
                int dy = y - sy;
                mLastTouchLocation[0] = x;
                mLastTouchLocation[1] = y;

                int tx = getScrollX() + (-dx);
                int ty = getScrollY() + (-dy);

                //TODO 检测y的边界,让他 不能滑倒底部继续滑动
                scrollTo(0, ty);
                postInvalidate();
                break;
        }

        return super.dispatchTouchEvent(event);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            child.measure(widthMeasureSpec, 0);//child 的 height 必须是 wrapcontent,由于此view允许滑动,所以子view都要计算出来
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //垂直布局
        int layoutLeft = 0;
        int layoutTop = 0;
        int layoutRight = r;
        int layoutBottom = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            layoutBottom = layoutTop + child.getMeasuredHeight();
            child.layout(layoutLeft, layoutTop, layoutRight, layoutBottom);
            layoutTop = layoutBottom;
        }
    }


}

这个滑动的到此结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值