使用Databinding轻松快速打造仿携程app筛选控件(三)

前面两章实现了筛选控件的内容,今天要来实现筛选控件的容器,首先看效果:

12.gif

需求分析

点击上面的按钮可以展开隐藏菜单,需要有动画效果,重复点击同一个按钮可以toggle,菜单展开时点击另外的按钮需要先收起菜单再展开菜单,点击半透明蒙层可以收起菜单。

实现思路

很多初级开发者可能看到这个效果会立刻想到用PopupWindow去实现,然而PopupWindow去实现有很多坑,例如点击隐藏的控制、代码书写麻烦,处理生命周期状态保存麻烦,和现有的代码控件协调使用等等。

网上也也有一些实现,例如 dongjunkun/DropDownMenu,阅读源码发现其代码陈旧,封装过于严密而缺乏灵活性,不建议用于生产环境。

按照kiss原则,我们的实现方式应该是简单的,我们采用的实现方式是移动View的方式实现菜单展开隐藏的效果。动画采用Animator

注意我们不采用动态修改View的高度去实现隐藏展开而是修改TranlationY是有原因的,如果动态修改高度会导致View的重新测量,像我们的View内部可能包含RecyclerView重新测量,会带来严重的性能问题

实现

  1. 首先定义一个自定义的ViewGroup,这个ViewGroup的背景色为灰色透明,相当于一个蒙层,他的子view我们通过DataBinding在调用的时候动态添加,这样保证了最大的灵活性:要呈现的内容完全开放给调用方,我们只负责展示和隐藏。
public class YokoView extends FrameLayout {


    public interface YokoAdapter {
        void onCollapsed(YokoView view);

        void onExpanded(YokoView view);
    }

    private View mView;
    private YokoAdapter mAdapter;
    private boolean animating;

    public YokoView(@NonNull Context context) {
        this(context, null);
    }

    public YokoView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    public ViewDataBinding initMenuView(@LayoutRes int resId) {
        removeAllViews();
        ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(getContext()),
                resId, this, true);
        mView = binding.getRoot();
        mView.setClickable(true);
        sync();
        setOnClickListener(v -> {
            if (!animating) {
                toggle();
            }
        });
        return binding;
    }

    public View getMenuView() {
        return mView;
    }

    public void sync() {
        setVisibility(isCollapsed() ? View.GONE : View.VISIBLE);
    }

    public void setApdater(YokoAdapter adapter) {
        this.mAdapter = adapter;
    }

    private void performCallback(int type) {
        if (mAdapter == null) {
            return;
        }
        if (type == 0) {
            mAdapter.onCollapsed(this);
        }
        if (type == 1) {
            mAdapter.onExpanded(this);
        }
    }


    public void collapse() {
        getMenuView().animate()
                .translationY(-getMenuView().getHeight())
                .withStartAction(() -> {
                    animating = true;
                })
                .withEndAction(() -> {
                    animating = false;
                    performCallback(0);
                    setVisibility(View.GONE);
                })
                .start();
    }

    public boolean isCollapsed() {
        return getMenuView().getTranslationY() <= -getMenuView().getHeight();
    }

    public boolean isExpanded() {
        return getMenuView().getTranslationY() >= 0;
    }

    public void expand(@Nullable Runnable beforeExpand) {
        getMenuView().animate()
                .translationY(0)
                .withStartAction(() -> {
                    animating = true;
                    setVisibility(View.VISIBLE);
                    if (beforeExpand != null) {
                        beforeExpand.run();
                    }
                })
                .withEndAction(() -> {
                    animating = false;
                    performCallback(1);
                })
                .start();
    }


    public void collapseThenExpand(@Nullable Runnable beforeExpand) {
        if (isCollapsed()) {
            expand(beforeExpand);
        } else {
            getMenuView().animate()
                    .translationY(-getMenuView().getHeight())
                    .withStartAction(() -> {
                        animating = true;
                    })
                    .withEndAction(() -> {
                        animating = false;
                        expand(beforeExpand);
                    })
                    .start();
        }
    }

    public void toggle() {
        if (isCollapsed()) {
            expand(null);
        } else {
            collapse();
        }
    }
}

使用

  1. 布局文件
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="checkedId"
            type="androidx.databinding.ObservableInt" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".yoko.YokoTestActivity">

        <TextView
            android:id="@+id/textView"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp"
            android:background="@color/colorPrimary"
            android:gravity="center"
            android:text="被覆盖的区域"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/toggle" />

        <github.hotstu.lib.hof.yokohama.YokoView
            android:id="@+id/yokoView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:background="#77000000"
            android:translationZ="1dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/toggle"
            app:layout_goneMarginTop="200dp">


        </github.hotstu.lib.hof.yokohama.YokoView>


        <Button
            android:id="@+id/toggle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:background="@drawable/hof_s_btn_bg"
            android:onClick="onClick"
            android:text="栏目0"
            app:checked="@{checkedId}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/cte"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:background="@drawable/hof_s_btn_bg"
            android:onClick="onClick"
            android:text="栏目1"
            app:checked="@{checkedId}"
            app:layout_constraintStart_toEndOf="@+id/toggle"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/cte2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:background="@drawable/hof_s_btn_bg"
            android:onClick="onClick"

            android:text="栏目2"
            app:checked="@{checkedId}"
            app:layout_constraintStart_toEndOf="@+id/cte"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/cte3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:background="@drawable/hof_s_btn_bg"
            android:onClick="onClick"
            android:text="栏目3"
            app:checked="@{checkedId}"
            app:layout_constraintStart_toEndOf="@+id/cte2"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
@Route(path = "/app/yoko", name = "抽屉菜单")
public class YokoTestActivity extends AppCompatActivity {

    private YokoView yokoView;
    private ObservableInt currentSelect = new ObservableInt();
    private ViewDataBinding mBinding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ViewDataBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_yoko_test);
        binding.setVariable(BR.checkedId, currentSelect);
        yokoView = findViewById(R.id.yokoView);
        yokoView.setApdater(new YokoView.YokoAdapter() {
            @Override
            public void onCollapsed(YokoView view) {
                Log.d("hof", "onCollapsed");
            }

            @Override
            public void onExpanded(YokoView view) {
                Log.d("hof", "onExpanded");
            }
        });
        mBinding = yokoView.initMenuView(R.layout.include_yoko_container_layout);

    }


    @BindingAdapter("bind:checked")
    public static void setChecked(View v, int checkedId) {
        if (v.getId() == checkedId) {
            v.setSelected(true);
        } else {
            v.setSelected(false);
        }
    }

    public void onClick(View view) {
        if (currentSelect.get() == view.getId()) {
            yokoView.toggle();
            return;
        }
        if (view.getId() == R.id.toggle) {
            yokoView.collapseThenExpand(() -> {
                mBinding.setVariable(BR.text, "栏目0内容");
            });
        }
        if (view.getId() == R.id.cte) {
            yokoView.collapseThenExpand(() -> {
                mBinding.setVariable(BR.text, "栏目1内容");

            });
        }
        if (view.getId() == R.id.cte2) {
            yokoView.collapseThenExpand(() -> {
                mBinding.setVariable(BR.text, "栏目2内容");

            });
        }
        if (view.getId() == R.id.cte3) {
            yokoView.collapseThenExpand(() -> {
                mBinding.setVariable(BR.text, "栏目3内容");

            });
        }
        currentSelect.set(view.getId());
    }
}

项目地址

github

其他

使用Databinding轻松快速打造仿携程app筛选控件(一)

使用Databinding轻松快速打造仿携程app筛选控件(二)

使用Databinding轻松快速打造仿携程app筛选控件(三)

more

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

推荐阅读更多精彩内容