高级UI工作生活Android自定义View

安卓:自定义View实现底部滑动布局(可配合Recyclervi

2019-08-17  本文已影响35人  仓鼠说他不会飞

前言:

忙忙碌碌一个月,加入公司地图引擎更换的任务中,忙里偷闲记录下实现的一个低配底部滑动布局,原本计划用的是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中剔除即可。

上一篇下一篇

猜你喜欢

热点阅读