在上一节之后,我们可以开始构建视图了。
我们可以制作一个构建器,这个构建器主要用来干嘛呢?通过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