使用Databinding輕鬆快速打造仿攜程app篩選控制元件(三)

hglfNg發表於2019-02-28

前面兩章實現了篩選控制元件的內容,今天要來實現篩選控制元件的容器,首先看效果:

2772606-f5f3c8754f853353.gif
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

相關文章