Android 自定义底部上拉控件的实现

来源:互联网 发布:矩阵的范数 编辑:程序博客网 时间:2024/06/05 07:10

前言

又到了新的一月,今天提供一个Android自定义底部上拉布局的实现,起因是自己在项目中需要实现这样一个控件,干脆自己写一个练练手。

写完了觉得能想到的需求都基本有了(可能会有其它需求,不过基本上改吧改吧就行了),又花了一点时间直接放到了Github上托管,希望能给您一些参考价值:

SlideBottomLayout-Android 简单易上手的Android底部上拉控件

先看一下实现效果:

这里写图片描述

分析一下这种控件的基本需求有以下几种:

1.有一个部分是能够作为把手(就是图中的handle,)进行拖拽的,这部分高度是暴露在界面中的 -> 需要实现:Handle按钮
* 特殊需求特殊分析,比如让这个Handle透明实现无Handle的效果

2.底部上啦布局是有一定高度限制的,不一定覆盖设备的整个屏幕 -> 需要自定义最大高度

3.当从底部上拉一点点时抬手,布局缩回,若超过一定高度,自动弹到最高,隐藏同理 -> 需要自定义自动到达顶部/隐藏的阈值

直接使用

直接使用也很简单,笔者进行了简单的封装,以供参考:

1. 在Project的build.gradle文件中添加:

allprojects {  repositories {    ...    maven { url 'https://jitpack.io' }  }}

2.在Module的build.gradle文件中添加:

dependencies {     compile 'com.github.qingmei2:SlideBottomLayout-Android:1.2.3'}

3.Add view in your layout:

需要注意的是:为了简单实现,笔者偷了个懒,设定为该布局下只能有一个直接的子View(类似ScrollView)
因此如果您添加需要一个布局,请在外面嵌套一个ViewGroup:

    <com.qingmei2.library.SlideBottomLayout        android:id="@+id/slideLayout"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_alignParentBottom="true"        android:layout_marginTop="200dp"        app:handler_height="50dp">        <!--app:handler_height:该属性就是您要暴露出来Handle的高度,详见下方的TextView(id=handle)-->        <!--Just one child-->        <LinearLayout            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:orientation="vertical">            <TextView                android:id="@+id/handle"                android:layout_width="match_parent"                android:layout_height="50dp"                android:background="#d95858"                android:gravity="center"                android:text="handle"                android:textColor="@android:color/white"                android:textSize="16sp" />            <android.support.v7.widget.RecyclerView                android:id="@+id/recycler_view"                android:layout_width="match_parent"                android:layout_height="wrap_content">            </android.support.v7.widget.RecyclerView>        </LinearLayout>    </com.qingmei2.library.SlideBottomLayout>

实现步骤

具体代码如下,其中上述需求的设置方式都已经在代码中声明:

先看下属性声明:

<?xml version="1.0" encoding="utf-8"?><resources>    <declare-styleable name="SlideBottomLayout">        <attr name="handler_height" format="dimension"></attr>    </declare-styleable></resources>
public class SlideBottomLayout extends LinearLayout {    public void setShortSlideListener(ShortSlideListener listener) {        this.shortSlideListener = listener;    }    private ShortSlideListener shortSlideListener;    /**     * The {@link MotionEvent#ACTION_DOWN} gesture location.     */    private int downY;    /**     * The {@link MotionEvent#ACTION_MOVE} gesture location.     */    private int moveY;    /**     * the value of moved distance by the gesture. When the value was modified and not exceed     * the {@link #movedMaxDis}, then make this ViewGroup move.     */    private int movedDis;    /**     * The max distance that the {@link SlideBottomLayout} can scroll to, it used to compare with the     * {@link #downY}, determine whether it can slide by the gesture.     */    private int movedMaxDis;    /**     * ChildView of the {@link SlideBottomLayout}, you can set a Layout such as the {@link LinearLayout}、     * {@link android.widget.RelativeLayout} ect.     * We set the rules that {@link SlideBottomLayout} just can have one child-view, or else get a     * {@link RuntimeException} at {@link #onFinishInflate()}     */    private View childView;    /**     * The control {@link SlideBottomLayout} automatically switches the threshold of the state. if     * this ViewGroup moved distance more than {@link #movedMaxDis} * it, switch the state of     * {@link #arriveTop} right now.     * </p>     * See the {@link #touchActionUp(float)}.     */    private float hideWeight = 0.25f;    //3.注意,这个接口用来设置「需要自定义自动到达顶部/隐藏的阈值」    public void setHideWeight(float hideWeight) {        if (hideWeight <= 0 || hideWeight > 1)            throw new IllegalArgumentException("hideWeight should belong (0f,1f]");        this.hideWeight = hideWeight;    }    private Scroller mScroller;    /**     * It means the {@link #childView} is arriving the top of parent or else.     */    private boolean arriveTop = false;    /**     * the {@link #childView} Initially visible height     */    private float visibilityHeight;    //1.初始化Handle显示高度,建议您在xml中设置对应属性来实现该效果    public void setVisibilityHeight(float visibilityHeight) {        this.visibilityHeight = visibilityHeight;    }    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);    }    /**     * Get the config from {@link R.styleable}, then init other configrations{@link #initConfig(Context)}.     *     * @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);    }    private void initConfig(Context context) {        if (mScroller == null)            mScroller = new Scroller(context);        this.setBackgroundColor(Color.TRANSPARENT);    }    /**     * It start a judgement for ensure the child-view  be unique in this method,then assgin it     * to {{@link #childView}.     * this method will be called before the {@link #onMeasure(int, int)}     */    @Override    protected void onFinishInflate() {        super.onFinishInflate();        if (getChildCount() == 0 || getChildAt(0) == null) {            throw new RuntimeException("there have no child-View in the SlideBottomLayout!");        }        if (getChildCount() > 1) {            throw new RuntimeException("there just alow one child-View in the SlideBottomLayout!");        }        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 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);    }    @Override    public void computeScroll() {        super.computeScroll();        if (mScroller == null)            mScroller = new Scroller(getContext());        if (mScroller.computeScrollOffset()) {            scrollTo(0, mScroller.getCurrY());            postInvalidate();        }    }    /**     * When the touch event is {@link MotionEvent#ACTION_UP},     * then judge the state of view group and control the {@link Scroller} to scroll.     * <p>     * In this ViewGroup, we set the rules that is if this scroll gesture's move distance     * more than {@link #movedMaxDis} * {@link #hideWeight}(default hideWeight value is 1/4 heights     * of this ViewGroup), then call {@link #hide()} or {@link #show()} right now. which method will     * be call depends on {@link #arriveTop}.     * <p     * if the scroll gesture's move distance don't reach the goal value, then call the     * {@link ShortSlideListener#onShortSlide(float)} if you call {@link #setShortSlideListener(ShortSlideListener)}     * init this ViewGroup. else will call {@link #hide()}.     *     * @param eventY The location of trigger     * @return Be used to determine consume this event or else.     */    public boolean touchActionUp(float eventY) {        if (movedDis > movedMaxDis * hideWeight) {            switchVisible();        } else {            if (shortSlideListener != null) {                shortSlideListener.onShortSlide(eventY);            } else {                hide();            }        }        return true;    }    /**     * When the touch event is {@link MotionEvent#ACTION_MOVE},     * then judge the state of view group and control the {@link Scroller} to scroll.     * <p>     * In this ViewGroup, we set the rules that is if this scroll gesture's move distance     * more than {@link #movedMaxDis} * {@link #hideWeight}(default hideWeight value is 1/4 heights of this ViewGroup),     * then call {@link #hide()} or {@link #show()} right now.     * <p>     *     * @param eventY The location of trigger     * @return Be used to determine consume this event or else.     */    public boolean touchActionMove(float eventY) {        moveY = (int) eventY;        //the dy is sum of the move distance,  the value > 0 means scroll up, the value < 0 means scroll down.        final int dy = downY - moveY;        if (dy > 0) {               //scroll up            movedDis += dy;            if (movedDis > movedMaxDis)                movedDis = movedMaxDis;            if (movedDis < movedMaxDis) {                scrollBy(0, dy);                downY = moveY;                return true;            }        } else {                //scroll down            movedDis += dy;            if (movedDis < 0) movedDis = 0;            if (movedDis > 0) {                scrollBy(0, dy);            }            downY = moveY;            return true;        }        return false;    }    /**     * When the touch event is {@link MotionEvent#ACTION_DOWN},     * Record the location of this action.     *     * @param eventY The location of trigger     * @return Be used to determine consume this event or else.     */    public boolean touchActionDown(float eventY) {        downY = (int) eventY;        //Whether custom this gesture.        if (!arriveTop && downY < movedMaxDis) {            return false;        } else            return true;    }    /**     * the extand method for showing {@link SlideBottomLayout}     */    public void show() {        scroll2TopImmediate();    }    /**     * the extand method for hiding {@link SlideBottomLayout}     */    public void hide() {        scroll2BottomImmediate();    }    /**     * @return The ViewGroup is arrive top or else.     */    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;    }    public void scroll2BottomImmediate() {        mScroller.startScroll(0, getScrollY(), 0, -getScrollY());        postInvalidate();        movedDis = 0;        arriveTop = false;    }}

注释也比较明了,如果有疑问,详细请参照SlideBottomLayout-Android 简单易上手的Android底部上拉控件

里面有相对详细的使用说明,此外,如果还有一些需求,您可以在issue中提出,提前感谢!

阅读全文
0 0
原创粉丝点击