安卓BottomSheet实现——动态加载视图

安卓BottomSheet实现——动态加载视图

在上一节之后,我们可以开始构建视图了。

构建器

我们可以制作一个构建器,这个构建器主要用来干嘛呢?通过Menu文件来使用适配器等工具来构建整个BottomSheet的视图。因为我们的控件想要做到不事先在xml中定义,整体都是使用代码来动态生成,即插即用型,所以我们就有了这么一个构建器来动态生成BottomSheet这一视图。新建 BottomSheetAdapterBuilder.java

public class BottomSheetAdapterBuilder {

    private List mItems;
    private int mTitles;
    private int mMode;
    private Menu mMenu;
    private boolean mFromMenu;
    private Context mContext;

    public BottomSheetAdapterBuilder(Context context) {
        mContext = context;
        mItems = new ArrayList<>();
    }

    public void setMenu(Menu menu) {
        mMenu = menu;
        mFromMenu = true;
    }

    public void setMode(int mode) {
        mMode = mode;
    }

    public void addTitleItem(String title, int titleTextColor) {
        mItems.add(new BottomSheetHeader(title, titleTextColor));
    }

    public void addDividerItem(int dividerBackground) {
        mItems.add(new BottomSheetDivider(dividerBackground));
    }

    public void addItem(int id, String title, Drawable icon, int itemTextColor,
                        int itemBackground, int tintColor) {
        if (mMenu == null) {
            mMenu = new MenuBuilder(mContext);
        }
        MenuItem item = mMenu.add(Menu.NONE, id, Menu.NONE, title);
        item.setIcon(icon);
        mItems.add(new BottomSheetMenuItem(item, itemTextColor, itemBackground, tintColor));
    }

    @SuppressLint("InflateParams")
    public View createView(int titleTextColor, int backgroundDrawable, int backgroundColor,
                           int dividerBackground, int itemTextColor, int itemBackground,
                           int tintColor, BottomSheetItemClickListener itemClickListener) {

        if (mFromMenu) {
            mItems = createAdapterItems(dividerBackground, titleTextColor,
                    itemTextColor, itemBackground, tintColor);
        }

        LayoutInflater layoutInflater = LayoutInflater.from(mContext);

        View sheet = mMode == BottomSheetBuilder.MODE_GRID ?
                layoutInflater.inflate(R.layout.bottomsheetbuilder_sheet_grid, null)
                : layoutInflater.inflate(R.layout.bottomsheetbuilder_sheet_list, null);

        final RecyclerView recyclerView = (RecyclerView) sheet.findViewById(R.id.recyclerView);
        recyclerView.setHasFixedSize(true);

        if (backgroundDrawable != 0) {
            sheet.setBackgroundResource(backgroundDrawable);
        } else {
            if (backgroundColor != 0) {
                sheet.setBackgroundColor(backgroundColor);
            }
        }

        // If we only have one title and it's the first item, set it as fixed
        if (mTitles == 1 && mMode == BottomSheetBuilder.MODE_LIST) {
            BottomSheetItem header = mItems.get(0);
            TextView headerTextView = (TextView) sheet.findViewById(R.id.textView);
            if (header instanceof BottomSheetHeader) {
                headerTextView.setVisibility(View.VISIBLE);
                headerTextView.setText(header.getTitle());
                if (titleTextColor != 0) {
                    headerTextView.setTextColor(titleTextColor);
                }
                mItems.remove(0);
            }
        }

        final BottomSheetItemAdapter adapter = new BottomSheetItemAdapter(mItems, mMode,
                itemClickListener);

        if (mMode == BottomSheetBuilder.MODE_LIST) {
            recyclerView.setLayoutManager(new LinearLayoutManager(mContext));
            recyclerView.setAdapter(adapter);
        } else {
            final int columns = mContext.getResources().getInteger(R.integer.bottomsheet_grid_columns);
            GridLayoutManager layoutManager = new GridLayoutManager(mContext, columns);
            recyclerView.setLayoutManager(layoutManager);
            recyclerView.post(new Runnable() {
                @Override
                public void run() {
                    float margin = mContext.getResources()
                            .getDimensionPixelSize(R.dimen.bottomsheet_grid_horizontal_margin);
                    adapter.setItemWidth((int) ((recyclerView.getWidth() - 2 * margin) / columns));
                    recyclerView.setAdapter(adapter);
                }
            });
        }

        return sheet;
    }

    public List getItems() {
        return mItems;
    }

    private List createAdapterItems(int dividerBackground, int titleTextColor,
                                                     int itemTextColor, int itemBackground,
                                                     int tintColor) {
        List items = new ArrayList<>();
        mTitles = 0;

        boolean addedSubMenu = false;

        for (int i = 0; i < mMenu.size(); i++) {
            MenuItem item = mMenu.getItem(i);

            if (item.isVisible()) {
                if (item.hasSubMenu()) {
                    SubMenu subMenu = item.getSubMenu();

                    if (i != 0 && addedSubMenu) {
                        if (mMode == BottomSheetBuilder.MODE_GRID) {
                            throw new IllegalArgumentException("MODE_GRID can't have submenus." +
                                    " Use MODE_LIST instead");
                        }
                        items.add(new BottomSheetDivider(dividerBackground));
                    }

                    CharSequence title = item.getTitle();
                    if (title != null && !title.equals("")) {
                        items.add(new BottomSheetHeader(title.toString(), titleTextColor));
                        mTitles++;
                    }

                    for (int j = 0; j < subMenu.size(); j++) {
                        MenuItem subItem = subMenu.getItem(j);
                        if (subItem.isVisible()) {
                            items.add(new BottomSheetMenuItem(subItem, itemTextColor,
                                    itemBackground, tintColor));
                            addedSubMenu = true;
                        }
                    }
                } else {
                    items.add(new BottomSheetMenuItem(item, itemTextColor, itemBackground, tintColor));
                }
            }
        }

        return items;
    }

}

首先我们来看 createAdapterItems 函数,该函数的作用就是从Menu中读取item来得到items。对于MenuItem是可能有SubMenu的,所以我们也必须要对这个进行检测。但是对于grid类型的是不能有子菜单的,所以我们会抛出一个错误。对于有子菜单的item,我们把他升级为标题类型,然后在读取子菜单项。

最重要的是 createView 函数,作用当然是动态创建BottomSheet了。首先是从Menu中读取出items,然后根据类型来构建出一个view对象。接下来有个特殊操作,如果只有一个标题的话,将其固定。

根据类型的不同,也使用不同LayoutManager,这一点也不需要解释,但是在grid类型中,需要计算每一个item的宽度是多少的小计算,减去两边的margin,中间的一除即可。然后搭配上适配器即可。这样的话整个BottomSheet的视图就动态搭建完成了。

动态搭建完成后,我们还需要将这个部件放进我们的主要视图中,这里必须要说明一点,BottomSheet必须要是在CoordinatorLayout中才能使用,因为他的一些behavior折叠操作都是需要在CoordinatorLayout中使用。

如何在主视图中显示

这里采用了一个从底部弹起的方式,所以继承了 BottomSheetDialog

public class BottomSheetMenuDialog extends BottomSheetDialog implements BottomSheetItemClickListener {

    BottomSheetBehavior.BottomSheetCallback mCallback;
    BottomSheetBehavior mBehavior;
    private BottomSheetItemClickListener mClickListener;
    private AppBarLayout mAppBarLayout;
    private boolean mExpandOnStart;
    private boolean mDelayDismiss;
    boolean mRequestedExpand;
    boolean mClicked;
    boolean mRequestCancel;
    boolean mRequestDismiss;
    OnCancelListener mOnCancelListener;

    public BottomSheetMenuDialog(Context context) {
        super(context);
    }

    public BottomSheetMenuDialog(Context context, int theme) {
        super(context, theme);
    }

    /**
     * Dismiss the BottomSheetDialog while animating the sheet.
     */
    public void dismissWithAnimation() {
        if (mBehavior != null) {
            mBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
        }
    }

    @Override
    public void setOnCancelListener(OnCancelListener listener) {
        super.setOnCancelListener(listener);
        mOnCancelListener = listener;
    }

    @Override
    public void cancel() {
        mRequestCancel = true;
        super.cancel();
    }

    @Override
    public void dismiss() {
        mRequestDismiss = true;
        if (mRequestCancel) {
            dismissWithAnimation();
        } else {
            super.dismiss();
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        final FrameLayout sheet = (FrameLayout) findViewById(R.id.design_bottom_sheet);

        if (sheet != null) {
            mBehavior = BottomSheetBehavior.from(sheet);
            mBehavior.setBottomSheetCallback(mBottomSheetCallback);
            mBehavior.setSkipCollapsed(true);

            if (getContext().getResources().getBoolean(R.bool.tablet_landscape)) {
                CoordinatorLayout.LayoutParams layoutParams
                        = (CoordinatorLayout.LayoutParams) sheet.getLayoutParams();
                layoutParams.width = getContext().getResources()
                        .getDimensionPixelSize(R.dimen.bottomsheet_width);
                sheet.setLayoutParams(layoutParams);
            }

            // Make sure the sheet doesn't overlap the appbar
            if (mAppBarLayout != null) {
                if (mAppBarLayout.getHeight() == 0) {
                    mAppBarLayout.getViewTreeObserver()
                            .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                                @Override
                                public void onGlobalLayout() {
                                    applyAppbarMargin(sheet);
                                }
                            });
                } else {
                    applyAppbarMargin(sheet);
                }
            }

            if (getContext().getResources().getBoolean(R.bool.landscape)) {
                fixLandscapePeekHeight(sheet);
            }

            if (mExpandOnStart) {
                sheet.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        mBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
                        if (mBehavior.getState() == BottomSheetBehavior.STATE_SETTLING
                                && mRequestedExpand) {
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                                sheet.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                            } else {
                                //noinspection deprecation
                                sheet.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                            }
                        }
                        mRequestedExpand = true;
                    }
                });
            }
        }
    }

    public void setAppBar(AppBarLayout appBar) {
        mAppBarLayout = appBar;
    }

    public void expandOnStart(boolean expand) {
        mExpandOnStart = expand;
    }

    public void delayDismiss(boolean dismiss) {
        mDelayDismiss = dismiss;
    }

    public void setBottomSheetCallback(BottomSheetBehavior.BottomSheetCallback callback) {
        mCallback = callback;
    }

    public void setBottomSheetItemClickListener(BottomSheetItemClickListener listener) {
        mClickListener = listener;
    }

    public BottomSheetBehavior getBehavior() {
        return mBehavior;
    }

    @Override
    public void onBottomSheetItemClick(MenuItem item) {
        if (!mClicked) {

            if (mBehavior != null) {
                if (mDelayDismiss) {
                    BottomSheetBuilderUtils.delayDismiss(mBehavior);
                } else {
                    mBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
                }
            }

            if (mClickListener != null) {
                mClickListener.onBottomSheetItemClick(item);
            }

            mClicked = true;
        }
    }

    private BottomSheetBehavior.BottomSheetCallback mBottomSheetCallback
            = new BottomSheetBehavior.BottomSheetCallback() {
        @Override
        public void onStateChanged(@NonNull View bottomSheet,
                                   @BottomSheetBehavior.State int newState) {

            if (mCallback != null) {
                mCallback.onStateChanged(bottomSheet, newState);
            }

            //noinspection WrongConstant
            if (newState == BottomSheetBehavior.STATE_HIDDEN) {
                mBehavior.setBottomSheetCallback(null);
                try {
                    BottomSheetMenuDialog.super.dismiss();
                }catch (IllegalArgumentException e){
                    // Ignore exception handling
                }

                // User dragged the sheet.
                if (!mClicked && !mRequestDismiss && !mRequestCancel && mOnCancelListener != null) {
                    mOnCancelListener.onCancel(BottomSheetMenuDialog.this);
                }
            }
        }

        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {
            if (mCallback != null) {
                mCallback.onSlide(bottomSheet, slideOffset);
            }
        }
    };

    private void fixLandscapePeekHeight(final View sheet) {
        // On landscape, we shouldn't use the 16:9 keyline alignment
        sheet.post(new Runnable() {
            @Override
            public void run() {
                mBehavior.setPeekHeight(sheet.getHeight() / 2);
            }
        });
    }

    private void applyAppbarMargin(View sheet) {
        CoordinatorLayout.LayoutParams layoutParams
                = (CoordinatorLayout.LayoutParams) sheet.getLayoutParams();
        layoutParams.topMargin = mAppBarLayout.getHeight();
        sheet.setLayoutParams(layoutParams);
    }
}

在onStart函数中的 R.id.design_bottom_sheet 是安卓自带的id来放置dialog。接下来的if判断,是否处于平板中,若是在平板中,会更改整个dialog的宽度。然后判断AppBar的高度,使BottomSheet不会盖过Appbar。还有横屏模式与自动开启的逻辑处理。

还有一些关于点击操作的处理,其中BottomSheetBuilderUtils是我们编写的一个辅助工具类,用来储存状态与延迟关闭,会有0.3秒的时间来延迟,是否开启这个功能由 mDelayDismiss 来决定。

BottomSheetBuilderUtils.java

public class BottomSheetBuilderUtils {

    public static final String SAVED_STATE = "saved_behavior_state";

    public static void delayDismiss(final BottomSheetBehavior behavior) {
        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {
                behavior.setState(BottomSheetBehavior.STATE_HIDDEN);
            }
        }, 300);
    }

    public static void saveState(Bundle outState, BottomSheetBehavior behavior) {
        if (outState != null) {
            outState.putInt(SAVED_STATE, behavior.getState());
        }
    }

    public static void restoreState(final Bundle savedInstanceState,
                                    final BottomSheetBehavior behavior) {
        if (savedInstanceState != null) {
            Handler handler = new Handler(Looper.getMainLooper());
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    int state = savedInstanceState.getInt(SAVED_STATE);
                    if (state == BottomSheetBehavior.STATE_EXPANDED && behavior != null) {
                        behavior.setState(state);
                    }
                }
            }, 300);
        }
    }
}

加载进主视图

主要的处理就是构建一个view来放入CoordinatorLayout的view中或dialog中,而构建view之前我们也已经写好。核心函数就是

 public View createView() {

        if (mMenu == null && mAdapterBuilder.getItems().isEmpty()) {
            throw new IllegalStateException("You need to provide at least one Menu " +
                    "or an item with addItem");
        }

        if (mCoordinatorLayout == null) {
            throw new IllegalStateException("You need to provide a coordinatorLayout" +
                    "so the view can be placed on it");
        }

        View sheet = mAdapterBuilder.createView(mTitleTextColor, mBackgroundDrawable,
                mBackgroundColor, mDividerBackground, mItemTextColor, mItemBackground,
                mIconTintColor, mItemClickListener);

        ViewCompat.setElevation(sheet, mContext.getResources()
                .getDimensionPixelSize(R.dimen.bottomsheet_elevation));

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            sheet.findViewById(R.id.fakeShadow).setVisibility(View.GONE);
        }

        CoordinatorLayout.LayoutParams layoutParams
                = new CoordinatorLayout.LayoutParams(CoordinatorLayout.LayoutParams.MATCH_PARENT,
                CoordinatorLayout.LayoutParams.WRAP_CONTENT);

        layoutParams.gravity = Gravity.CENTER_HORIZONTAL;
        layoutParams.setBehavior(new BottomSheetBehavior());

        if (mContext.getResources().getBoolean(R.bool.tablet_landscape)) {
            layoutParams.width = mContext.getResources()
                    .getDimensionPixelSize(R.dimen.bottomsheet_width);
        }

        mCoordinatorLayout.addView(sheet, layoutParams);
        mCoordinatorLayout.postInvalidate();
        return sheet;
    }

    public BottomSheetMenuDialog createDialog() {

        if (mMenu == null && mAdapterBuilder.getItems().isEmpty()) {
            throw new IllegalStateException("You need to provide at least one Menu " +
                    "or an item with addItem");
        }

        BottomSheetMenuDialog dialog = mTheme == 0
                ? new BottomSheetMenuDialog(mContext, R.style.BottomSheetBuilder_DialogStyle)
                : new BottomSheetMenuDialog(mContext, mTheme);

        if (mTheme != 0) {
            setupThemeColors(mContext.obtainStyledAttributes(mTheme, new int[]{
                    R.attr.bottomSheetBuilderBackgroundColor,
                    R.attr.bottomSheetBuilderItemTextColor,
                    R.attr.bottomSheetBuilderTitleTextColor}));
        } else {
            setupThemeColors(mContext.getTheme().obtainStyledAttributes(new int[]{
                    R.attr.bottomSheetBuilderBackgroundColor,
                    R.attr.bottomSheetBuilderItemTextColor,
                    R.attr.bottomSheetBuilderTitleTextColor,}));
        }

        View sheet = mAdapterBuilder.createView(mTitleTextColor, mBackgroundDrawable,
                mBackgroundColor, mDividerBackground, mItemTextColor, mItemBackground,
                mIconTintColor, dialog);

        sheet.findViewById(R.id.fakeShadow).setVisibility(View.GONE);
        dialog.setAppBar(mAppBarLayout);
        dialog.expandOnStart(mExpandOnStart);
        dialog.delayDismiss(mDelayedDismiss);
        dialog.setBottomSheetItemClickListener(mItemClickListener);

        if (mContext.getResources().getBoolean(R.bool.tablet_landscape)) {
            FrameLayout.LayoutParams layoutParams
                    = new FrameLayout.LayoutParams(mContext.getResources()
                    .getDimensionPixelSize(R.dimen.bottomsheet_width),
                    ViewGroup.LayoutParams.WRAP_CONTENT);
            dialog.setContentView(sheet, layoutParams);
        } else {
            dialog.setContentView(sheet);
        }

        return dialog;
    }

这样的话,我们整个BottomSheet就构建完成了。

在项目中使用

    public void onShowDialogGridClick() {
        if (mBottomSheetDialog != null) {
            mBottomSheetDialog.dismiss();
        }
        mShowingGridDialog = true;
        mBottomSheetDialog = new BottomSheetBuilder(this, R.style.AppTheme_BottomSheetDialog)
                .setMode(BottomSheetBuilder.MODE_GRID)
                .setAppBarLayout(mAppBarLayout)
                .setMenu(getResources().getBoolean(R.bool.tablet_landscape)
                        ? R.menu.menu_bottom_grid_tablet_sheet : R.menu.menu_bottom_grid_sheet)
                .expandOnStart(true)
                .setItemClickListener(new BottomSheetItemClickListener() {
                    @Override
                    public void onBottomSheetItemClick(MenuItem item) {
                        mShowingGridDialog = false;
                    }
                })
                .createDialog();

        mBottomSheetDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface dialog) {
                mShowingGridDialog = false;
            }
        });
        mBottomSheetDialog.show();
    }

然后设置点击事件调用函数即可。

本文github地址参考:

https://github.com/rubensousa/BottomSheetBuilder

你可能感兴趣的:(Android)