安卓:自定义View实现底部滑动布局(可配合Recyclerview使用)

前言:

忙忙碌碌一个月,加入公司地图引擎更换的任务中,忙里偷闲记录下实现的一个低配底部滑动布局,原本计划用的是BottomSheet,可无奈,产品需求+控件使用环境,不得不自定义View来解决问题。

实现思路

实现思路相对坎坷,首次实现无法响应Recyclerview的滑动,在滑动拦截处理过后,算是完善了个版本。
实现的效果如下(重新写的damo):

ezgif.com-optimize.gif

使用方式
注意。。。。SlideBottomLayout只允许一个直接子布局,因为实现方式的原因,因为要使用SlideBottomLayout的子布局实现功能(如果看官觉得很鸡肋,就当我抛砖引玉)。
xml:

<ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@drawable/cat_back"/>
    <com.example.testativity.SlideBottomLayout
        android:id="@+id/slideBottom"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        app:handler_height="100dp">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="500dp"
            android:orientation="vertical"
            android:background="#FFFFFF">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:gravity="center">
                <View
                    android:layout_width="60dp"
                    android:layout_height="5dp"
                    android:background="@drawable/top_gray_sign"/>
            </LinearLayout>
            <android.support.v7.widget.RecyclerView
                android:id="@+id/rc"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
        </LinearLayout>
    </com.example.testativity.SlideBottomLayout>

java:

 SlideBottomLayout slideBottom = (SlideBottomLayout)findViewById(R.id.slideBottom);
        RecyclerView rc = (RecyclerView)findViewById(R.id.rc);
        slideBottom.bindRecyclerView(rc);
        rc.setLayoutManager(new LinearLayoutManager(this));
        List<String> list = new ArrayList<>();
        for (int i = 0;i<30;i++){
            list.add("时光不老,我们不散!");
        }
        final SlideBottomRcAdapter adapter = new SlideBottomRcAdapter(list,this);
        adapter.setListener(new SlideBottomRcAdapter.MultipleChoiceListener() {
            @Override
            public void setEvent(int position) {
                adapter.Update(position);
            }
        });
        rc.setAdapter(adapter);
        rc.setOverScrollMode(View.OVER_SCROLL_NEVER);

原理如下:测量滑动距离把布局画在滑动距离之下,通过onTouchEvent方法中对滑动事件进行判断。具体判断加注释已经在代码里面了。

@Override
    public boolean onTouchEvent(MotionEvent event) {
        final float dy = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (touchActionDown(dy)) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (touchActionMove(dy)) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
                if (touchActionUp(dy)) {
                    return true;
                }
                break;
        }
        return super.onTouchEvent(event);
    }

对不同情况下的判断,interceptTouchEvent方法进行拦截交予onTouchEvent处理
代码片段如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        float interceptY = ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                RecordY(interceptY);
                break;
            case MotionEvent.ACTION_MOVE:
                if(interceptJudge(interceptY)){
                    return onTouchEvent(ev);
                }
                return false;
            case MotionEvent.ACTION_UP:
                if(interceptJudge(interceptY)){
                    return onTouchEvent(ev);
                }
                return false;
        }
        return super.onInterceptTouchEvent(ev);
    }

贴出全部代码,内容注释清楚,以便下次观看一目了然(SlideBottomLayout类)

public class SlideBottomLayout extends LinearLayout {

    /**
     * 手势按下位置记录
     */
    private float downY;
    /**
     * 手势移动位置记录
     */
    private float moveY;
    /**
     * 手势移动距离
     */
    private int movedDis;
    /**
     * 移动的最大值
     */
    private int movedMaxDis;
    /**
     * SlideBottom 的子视图
     */
    private View childView;
    /**
     * SlideBottom状态
     * isShow的两种状态 伸张与收缩
     */
    private Boolean isShow = false;
    /**
     * 状态切换阈值
     */
    private float hideWeight = 0.3f;
    /**
     * 拦截器参数相关
     * 记录Action.Down按下位置
     * @param hideWeight
     */
    private int CurrentY;

    /**
     * 视图滚动辅助
     */
    private Scroller mScroller;

    /**
     *
     * 标记:childView到达parent或者其他的顶部
     */
    private boolean arriveTop = false;

    /**
     * 设置:childView的初始可见高度
     */
    private float visibilityHeight;
    /**
     * 绑定的Rc
     */
    private RecyclerView recyclerview;

    private ShortSlideListener shortSlideListener;


    public SlideBottomLayout(@NonNull Context context) {
        super(context);
    }

    public SlideBottomLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initAttrs(context, attrs);
    }

    public SlideBottomLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttrs(context, attrs);
    }

    /**
     *初始化属性配置
     * @param context the {@link Context}
     * @param attrs   the configs in layout attrs.
     */
    private void initAttrs(Context context, AttributeSet attrs) {
        final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlideBottomLayout);
        visibilityHeight = ta.getDimension(R.styleable.SlideBottomLayout_handler_height, 0);
        ta.recycle();

        initConfig(context);
    }

    /**
     *  实现视图平滑滚动利器
     * @param context
     */
    private void initConfig(Context context) {
        if (mScroller == null) {
            mScroller = new Scroller(context);
        }
    }

    /**
     * 使用前判断/单一子视图
     * 该方法在OnMeasure(int,int)调用
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (getChildCount() == 0 || getChildAt(0) == null) {
            throw new RuntimeException("SlideBottom里面没有子布局");
        }
        if (getChildCount() > 1) {
            throw new RuntimeException("SlideBottom里只可以放置一个子布局");
        }
        childView = getChildAt(0);
    }

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

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        float interceptY = ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                RecordY(interceptY);
                break;
            case MotionEvent.ACTION_MOVE:
                if(interceptJudge(interceptY)){
                    return onTouchEvent(ev);
                }
                return false;
            case MotionEvent.ACTION_UP:
                if(interceptJudge(interceptY)){
                    return onTouchEvent(ev);
                }
                return false;
        }
        return super.onInterceptTouchEvent(ev);
    }


    /**
     * 记录下拦截器传来的Y值
     * @param interceptY
     */
    private void RecordY(float interceptY) {
        CurrentY = (int)interceptY;
    }

    /**
     * 拦截判断
     * @param interceptY
     * @return
     */
    private boolean interceptJudge(float interceptY) {
        float judgeY = CurrentY - interceptY;
        if(judgeY > 0){
            //向上滑动
            if(!arriveTop()){
                return true;
            }
        }
        if(judgeY < 0){
            //向下滑动
            if(arriveTop() && isTop(recyclerview)){
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final float dy = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (touchActionDown(dy)) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (touchActionMove(dy)) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
                if (touchActionUp(dy)) {
                    return true;
                }
                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     * scroll的更新方法
     * computeScrollOffset 返回true表示动画未完成
     */
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller == null)
            mScroller = new Scroller(getContext());
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            postInvalidate();
        }
    }


    public boolean touchActionUp(float eventY) {
        //移动的位置是否大于阈值
        if (movedDis > movedMaxDis * hideWeight) {
            switchVisible();
        } else {
            //提供一个接口用于处理没有达到阈值的手势
            if (shortSlideListener != null) {
                shortSlideListener.onShortSlide(eventY);
            } else {
                hide();
            }
        }
        return true;
    }

    public boolean touchActionMove(float eventY) {
        moveY =  eventY;
        //dy是移动距离的和 如果它的值>0表示向上滚动  <0表示向下滚动
        final float dy = downY - moveY;
        if (dy > 0) {               //向上
            movedDis += dy;
            if (movedDis > movedMaxDis) {
                movedDis = movedMaxDis;
            }

            if (movedDis < movedMaxDis) {
                scrollBy(0, (int) dy);
                downY = moveY;
                return true;
            }
        } else {                //向下
            movedDis += dy;
            if (movedDis < 0) {
                movedDis = 0;
            }
            if (movedDis > 0) {
                scrollBy(0, (int)dy);
            }
            downY = moveY;
            return true;
        }
        return false;
    }

    public boolean touchActionDown(float eventY) {
        //记录手指按下的位置
        downY = (int) eventY;

        if (!arriveTop && downY < movedMaxDis) {
            return false;
        } else{
            return true;
        }
    }

    /**
     * slidBottom的显示方法
     */
    public void show() {
        scroll2TopImmediate();
    }

    /**
     * slidBottom的隐藏方法
     */
    public void hide() {
        scroll2BottomImmediate();
    }

    /**
     * arriveTop返回值
     * 判断child是否到达顶部
     */
    public boolean switchVisible() {
        if (arriveTop()) {
            hide();
        } else {
            show();
        }
        return arriveTop();
    }

    public boolean arriveTop() {
        return this.arriveTop;
    }

    public void scroll2TopImmediate() {
        mScroller.startScroll(0, getScrollY(), 0, (movedMaxDis - getScrollY()));
        invalidate();
        movedDis = movedMaxDis;
        arriveTop = true;
        isShow= true;
    }

    public void scroll2BottomImmediate() {
        mScroller.startScroll(0, getScrollY(), 0, -getScrollY());
        postInvalidate();
        movedDis = 0;
        arriveTop = false;
        isShow = false;
    }



    /**
     * 绑定Recyclerview如果你的子布局中含有Recyclerview的话
     * 该方法用于判断是否到达Recyclerview的顶部
     * @param recyclerView
     * @return
     */
    public static boolean isTop(RecyclerView recyclerView){
        if(recyclerView == null){
            return false;
        }
        return !recyclerView.canScrollVertically(-1);
    }

    /**
     * 绑定RecyclerView(可选)
     * 如果子布局有RecyclerView必须绑定否则Recyclerview的滑动不会被拦截
     * @param recyclerView
     */
    public void bindRecyclerView(RecyclerView recyclerView){
        this.recyclerview = recyclerView;
    }

    public void setShortSlideListener(ShortSlideListener listener) {
        this.shortSlideListener = listener;
    }

    /**
     * 隐藏比重阈值
     * @param hideWeight
     */
    public void setHideWeight(float hideWeight) {
        if (hideWeight <= 0 || hideWeight > 1) {
            throw new IllegalArgumentException("隐藏的阈值应该在(0f,1f]之间");
        }
        this.hideWeight = hideWeight;
    }

    /**
     * 设置显示高度
     * @param visibilityHeight
     */
    public void setVisibilityHeight(float visibilityHeight) {
        this.visibilityHeight = visibilityHeight;
    }


}

写在最后

关于阈值,是控制滑动距离展示隐藏的临界值,没有动态设置,简单的set方法或者attributeset都可以。
关于ListView使用,参考Recyclerview的使用。
对于点击顶部隐藏,如果之后要改只需要在OnTouchEvent中剔除即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343