自定义View探究-一个简单的垂直上拉下滑View

一、简介
最近朋友公司需要实现一个垂直上拉下滑的View,该View最初只有一部分显示在屏幕最下方,上拉那一部分可以将该View全部拉出来并全部显示在屏幕上,下滑该View可以将该View隐藏在屏幕下。
先看一下最终实现效果吧。
忽略渣效果

二、实现思路
1、这个效果其实有很多实现方法,为了让松手时有一个viewpager一样的缓慢滑动的效果我选择用scrollBy配合Scroller,应该是既方便又实用的。
2、这个View的设计是这样的:
(1)将这个View的子view通过layout放在该View下面;
(2)通过重写onTouchEvent方法给这个子View滑动效果,在MOVE_UP的动作给这个子View加上Scroller平滑到View的顶部或者底部。
见图:
这里写图片描述

三、实现
1、先自定义一个属性,表示子View应该有多少部分露在外面,也就是上图中红色和绿色相交的部分。
在res文件夹-values文件夹下面创建一个attrs.xml文件
attrs.xml :

<resources>
    <declare-styleable name="MyScrollerView">
        <attr name="visibility_height" format="dimension"></attr>
    </declare-styleable>

</resources>

在XML文件中引用该属性:

<com.zw.myfirstapp.MyScrollerView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:background="@android:color/transparent"
            android:id="@+id/msv"
            app:visibility_height="100dp"
            ></com.zw.myfirstapp.MyScrollerView>

在代码中调用该属性(该View名字为MyScrollerView,我图方便继承的是LinearLayout,继承ViewGroup或者其他的布局View都可以):

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

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

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

        TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.MyScrollerView);
        visibilityHeight = ta.getDimension(R.styleable.MyScrollerView_visibility_height,200);
        ta.recycle();

        init(context);
    }

2、重写onFinishInflate方法,重写该方法的原因是我希望我只有一个子View,这样就好确定滑动的高度,不然我还需要重新计算子View们的高度总和,比较麻烦。这个方法会在onMeasure之前调用。

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if(getChildCount() == 0 || getChildAt(0) == null){
            throw new RuntimeException("没有子控件!");
        }
        if(getChildCount() > 1){
            throw new RuntimeException("只能有一个子控件!");
        }
        mChild = getChildAt(0);
    }

3、init方法里做一些初始化操作,比如说创建一个Scroller对象,给View的背景设为透明:

    private void init(Context context) {
        mScroller = new Scroller(context);
        this.setBackgroundColor(Color.TRANSPARENT);
    }

4、重写onMeasure方法和onLayout方法,确定可以滑动的最大高度,以及子View的排列位置(其实可以不用重写onMeasure,我这样写只是习惯)。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mScrollHeight = (int) (mChild.getMeasuredHeight() - visibilityHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        mChild.layout(0,mScrollHeight,mChild.getMeasuredWidth(),mChild.getMeasuredHeight() + mScrollHeight);
    }

5、先看我定义的成员变量的含义吧:

    /**
     * downY:手指按下时距离View顶部的距离
     * moveY:手指在屏幕上滑动的距离(不停变化)
     * movedY:手指在屏幕上总共滑动的距离(为了确定手指一共滑动了多少距离,不能超过可滑动的最大距离)
     */
    private int downY,moveY,movedY;

    //子View
    private View mChild;

    private Scroller mScroller;

    //可滑动的最大距离
    private int mScrollHeight;

    //子View是否在顶部
    private boolean isTop = false;

    //最初子View在View内可见的高度
    private float visibilityHeight;

6、重写onTouchEvent方法,做滑动判断,解释都写在注释里了:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //手指按下时距离View上面的距离
                downY = (int) event.getY();

                //如果子View不在顶部 && 按下的位置在子View没有显示的位置,则不消费此次滑动事件,否则消费
                if(!isTop && downY < mScrollHeight ){
                    return super.onTouchEvent(event);
                }
                return true;
            case MotionEvent.ACTION_MOVE:

                moveY = (int) event.getY();
                //deY是滑动的距离,向上滑时deY>0 ,向下滑时deY<0
                int deY = downY - moveY;

                //向上滑动时的处理
                if(deY > 0){
                    //将每次滑动的距离相加,为了防止子View的滑动超过View的顶部
                    movedY += deY;
                    if(movedY > mScrollHeight) movedY = mScrollHeight;

                    if(movedY < mScrollHeight){
                        scrollBy(0,deY);
                        downY = moveY;
                        return true;
                    }
                }

                //向下滑动时的处理,向下滑动时需要判断子View是否在顶部,如果不在顶部则不消费此次事件
                if(deY < 0 && isTop){
                    movedY += deY;
                    if(movedY < 0 ) movedY = 0;
                    if(movedY > 0){
                        scrollBy(0,deY);
                    }
                    downY = moveY;
                    return true;
                }

                break;
            case MotionEvent.ACTION_UP:
                //手指抬起时的处理,如果向上滑动的距离超过了最大可滑动距离的1/4,并且子View不在顶部,就表示想把它拉上去
                if(movedY > mScrollHeight / 4 && !isTop){
                    mScroller.startScroll(0,getScrollY(),0,(mScrollHeight - getScrollY()));
                    invalidate();
                    movedY = mScrollHeight;
                    isTop = true;
                }else {
                    //否则就表示放弃本次滑动,让它滑到最初的位置
                    mScroller.startScroll(0,getScrollY(),0, -getScrollY());
                    postInvalidate();
                    movedY = 0;
                    isTop = false;
                }

                break;
        }
        return super.onTouchEvent(event);
    }

7、最后要重写一个computeScroll方法,该方法是用来配合scroller的:

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
            scrollTo(0,mScroller.getCurrY());
            postInvalidate();
        }
    }

8、关于scroller的用法,可参考郭霖的这篇博客:
http://blog.csdn.net/guolin_blog/article/details/48719871

四、完整代码:
xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

        <com.zw.myfirstapp.MyScrollerView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:background="@android:color/transparent"
            android:id="@+id/msv"
            app:visibility_height="100dp"
            >
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:background="@mipmap/b"
                android:gravity="center"
                android:orientation="vertical">
                <Button
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/btn"
                    android:text="我是一个按钮"/>
            </LinearLayout>

        </com.zw.myfirstapp.MyScrollerView>

</RelativeLayout>

MyScrollerView:

public class MyScrollerView extends LinearLayout {

    /**
     * downY:手指按下时距离View顶部的距离
     * moveY:手指在屏幕上滑动的距离(不停变化)
     * movedY:手指在屏幕上总共滑动的距离(为了确定手指一共滑动了多少距离,不能超过可滑动的最大距离)
     */
    private int downY,moveY,movedY;

    //子View
    private View mChild;

    private Scroller mScroller;

    //可滑动的最大距离
    private int mScrollHeight;

    //子View是否在顶部
    private boolean isTop = false;

    //最初子View在View内可见的高度
    private float visibilityHeight;

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

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

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

        TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.MyScrollerView);
        visibilityHeight = ta.getDimension(R.styleable.MyScrollerView_visibility_height,200);
        ta.recycle();

        init(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mScrollHeight = (int) (mChild.getMeasuredHeight() - visibilityHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        mChild.layout(0,mScrollHeight,mChild.getMeasuredWidth(),mChild.getMeasuredHeight() + mScrollHeight);
    }

    private void init(Context context) {
        mScroller = new Scroller(context);
        this.setBackgroundColor(Color.TRANSPARENT);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if(getChildCount() == 0 || getChildAt(0) == null){
            throw new RuntimeException("没有子控件!");
        }
        if(getChildCount() > 1){
            throw new RuntimeException("只能有一个子控件!");
        }
        mChild = getChildAt(0);
    }



    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //手指按下时距离View上面的距离
                downY = (int) event.getY();

                //如果子View不在顶部 && 按下的位置在子View没有显示的位置,则不消费此次滑动事件,否则消费
                if(!isTop && downY < mScrollHeight ){
                    return super.onTouchEvent(event);
                }
                return true;
            case MotionEvent.ACTION_MOVE:

                moveY = (int) event.getY();
                //deY是滑动的距离,向上滑时deY>0 ,向下滑时deY<0
                int deY = downY - moveY;

                //向上滑动时的处理
                if(deY > 0){
                    //将每次滑动的距离相加,为了防止子View的滑动超过View的顶部
                    movedY += deY;
                    if(movedY > mScrollHeight) movedY = mScrollHeight;

                    if(movedY < mScrollHeight){
                        scrollBy(0,deY);
                        downY = moveY;
                        return true;
                    }
                }

                //向下滑动时的处理,向下滑动时需要判断子View是否在顶部,如果不在顶部则不消费此次事件
                if(deY < 0 && isTop){
                    movedY += deY;
                    if(movedY < 0 ) movedY = 0;
                    if(movedY > 0){
                        scrollBy(0,deY);
                    }
                    downY = moveY;
                    return true;
                }

                break;
            case MotionEvent.ACTION_UP:
                //手指抬起时的处理,如果向上滑动的距离超过了最大可滑动距离的1/4,并且子View不在顶部,就表示想把它拉上去
                if(movedY > mScrollHeight / 4 && !isTop){
                    mScroller.startScroll(0,getScrollY(),0,(mScrollHeight - getScrollY()));
                    invalidate();
                    movedY = mScrollHeight;
                    isTop = true;
                }else {
                    //否则就表示放弃本次滑动,让它滑到最初的位置
                    mScroller.startScroll(0,getScrollY(),0, -getScrollY());
                    postInvalidate();
                    movedY = 0;
                    isTop = false;
                }

                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
            scrollTo(0,mScroller.getCurrY());
            postInvalidate();
        }
    }
}

猜你喜欢

转载自blog.csdn.net/vicwudi/article/details/54645471