首页 > 学院 > 开发设计 > 正文

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

2019-11-07 23:57:25
字体:
来源:转载
供稿:网友

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

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

构建器

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

public class BottomSheetAdapterBuilder { PRivate List<BottomSheetItem> 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<BottomSheetItem> getItems() { return mItems; } private List<BottomSheetItem> createAdapterItems(int dividerBackground, int titleTextColor, int itemTextColor, int itemBackground, int tintColor) { List<BottomSheetItem> 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.javapublic 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


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表