BottomSheetLayout
BottomSheet:Google在API 23中已经加入了这样的一个控件。
BottomSheet介绍:
BottomSheet是一个可以从底部飞入和飞出的Android控件,通常是用于替换Dialog和menu(如:分享menu)
这里我介绍的并非是Android的一个这样的控件,而是Flipboard开元的这样的一个三方框架源码;
首先地址:
https://github.com/Flipboard/bottomsheet
建议大家看这篇博客之前先看下这官方的使用说明Usage,以便更好的理解下面介绍的内容.
效果图github上边基本都有了这里就不在二次粘图了,不是程序员该做的事.....
现在咱们就基于这个仓库的三方框架进行介绍:
导入之后的项目结构截图:
大家可以看到这里三个module(不要问我什么是module,信不信我会打死你....)
简单说下这三个module的作用啊
bottomsheet:这里主要说的是BottomSheet.xml,首先它是一个view,继承自Framelayout,你只需给他一个view就可以实现了飞入和飞出效果。
bottomsheet-commons:见下图红线内容区:底部飞入所需要的内容区域的布局Gridview,Listview,Fragment等也就是需要给bottomsheet的内容view。
bottomsheet-sample:不需要多说你懂得,6666,不知道我也是没办法教你了....
接下来比较吊了;
照图说话boottomsheet module;
先说背景色的实现代码:
private void init() {
............
dimView = new View(getContext());
dimView.setBackgroundColor(Color.BLACK);
dimView.setAlpha(0);
dimView.setVisibility(INVISIBLE);
.................
}
很明显是个view,透明度默认是0,可见性invisible。so 什么是可见的哪。
private void setSheetTranslation(float sheetTranslation) {
this.sheetTranslation = sheetTranslation;
int bottomClip = (int) (getHeight() - Math.ceil(sheetTranslation));
this.contentClipRect.set(0, 0, getWidth(), bottomClip);
getSheetView().setTranslationY(getHeight() - sheetTranslation);
transformView(sheetTranslation);
if (shouldDimContentView) {
float dimAlpha = getDimAlpha(sheetTranslation);
dimView.setAlpha(dimAlpha);//根据当前平移的位置换算当前view透明度
dimView.setVisibility(dimAlpha > 0 ? VISIBLE : INVISIBLE);//这里看是否透明度大于0,如果是那么久设置为可见
}
}
这里说的是当bottomsheet从底部出现之后,设置背景view可见同事修改透明度,当然透明度是根当前平移的位置换算粗来的。紧接着就是判断是否背景view的透明度大于零,如果是那么就直接设置背景view可见。
这里还有一个问题这个view是如何添加到BottomSheet中的哪?
先看一种设置方式:
/**
* Set the content view of the bottom sheet. This is the view which is shown under the sheet
* being presented. This is usually the root view of your application.
*
* @param contentView The content view of your application.
*/
public void setContentView(View contentView) {
super.addView(contentView, -1, generateDefaultLayoutParams());
super.addView(dimView, -1, generateDefaultLayoutParams());
}
这里调用地方法setContentView,如果这种方式的需要你出入一个contentView,当然这里不需要你自己去调用,这里并不是说你不可以自己调用。那么如果不是自己调用那应该怎么调用哪???还是看代码:
1 @Override 2 public void addView(@NonNull View child) { 3 if (getChildCount() > 0) { 4 throw new IllegalArgumentException("You may not declare more then one child of bottom sheet. The sheet view must be added dynamically with showWithSheetView()"); 5 } 6 setContentView(child); 7 }
代码其实很简单就是封装了一层,最终还是调用了method setContentView方法。不到大家发现了个问题了木,这个是方法是重载的方式,辣么为啥这么做类,其实是因为当BottomSheetLayout被add到一个父布局的时候需要调到这块,一般这块是系统调用,因为我们一般使用的时候都在在xml文件中使用。文章开头说道这个布局的父布局是FrameLayout,这样就有个好处,可以使用BottomSheetLayout作为root布局,其实根据官方demo来看,确实也是建议最好这么使用的。顺便说下额外的(使用Framelayout作为root布局的好处:对比RelativeLayout绘制上来说绘制次数:FrameLayout 绘制一次 而RelativeLayout绘制两次,所以FrameLayout的效率较高。至于为什么那就需要查源码了。。。。)
接下来就是BottomSheet中的内容view了,看代码:
1 /** 2 * Convenience for showWithSheetView(sheetView, null, null). 3 * 4 * @param sheetView The sheet to be presented. 5 */ 6 public void showWithSheetView(View sheetView) { 7 showWithSheetView(sheetView, null); 8 } 9 10 /** 11 * Present a sheet view to the user. 12 * If another sheet is currently presented, it will be dismissed, and the new sheet will be shown after that 13 * 14 * @param sheetView The sheet to be presented. 15 * @param viewTransformer The view transformer to use when presenting the sheet. 16 */ 17 public void showWithSheetView(final View sheetView, final ViewTransformer viewTransformer) { 18 if (state != State.HIDDEN) { 19 Runnable runAfterDismissThis = new Runnable() { 20 @Override 21 public void run() { 22 showWithSheetView(sheetView, viewTransformer); 23 } 24 }; 25 dismissSheet(runAfterDismissThis); 26 return; 27 } 28 setState(State.PREPARING); 29 30 LayoutParams params = (LayoutParams) sheetView.getLayoutParams(); 31 if (params == null) { 32 params = new LayoutParams(isTablet ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL); 33 } 34 35 if (isTablet && params.width == FrameLayout.LayoutParams.WRAP_CONTENT) { 36 37 // Center by default if they didn't specify anything 38 if (params.gravity == -1) { 39 params.gravity = Gravity.CENTER_HORIZONTAL; 40 } 41 42 params.width = defaultSheetWidth; 43 44 // Update start and end coordinates for touch reference 45 int horizontalSpacing = screenWidth - defaultSheetWidth; 46 sheetStartX = horizontalSpacing / 2; 47 sheetEndX = screenWidth - sheetStartX; 48 } 49 50 super.addView(sheetView, -1, params); 51 initializeSheetValues(); 52 this.viewTransformer = viewTransformer; 53 54 // Don't start animating until the sheet has been drawn once. This ensures that we don't do layout while animating and that 55 // the drawing cache for the view has been warmed up. tl;dr it reduces lag. 56 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 57 @Override 58 public boolean onPreDraw() { 59 getViewTreeObserver().removeOnPreDrawListener(this); 60 post(new Runnable() { 61 @Override 62 public void run() { 63 // Make sure sheet view is still here when first draw happens. 64 // In the case of a large lag it could be that the view is dismissed before it is drawn resulting in sheet view being null here. 65 if (getSheetView() != null) { 66 peekSheet(); 67 } 68 } 69 }); 70 return true; 71 } 72 }); 73 74 // sheetView should always be anchored to the bottom of the screen 75 currentSheetViewHeight = sheetView.getMeasuredHeight(); 76 sheetViewOnLayoutChangeListener = new OnLayoutChangeListener() { 77 @Override 78 public void onLayoutChange(View sheetView, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 79 int newSheetViewHeight = sheetView.getMeasuredHeight(); 80 if (state != State.HIDDEN && newSheetViewHeight < currentSheetViewHeight) { 81 // The sheet can no longer be in the expanded state if it has shrunk 82 if (state == State.EXPANDED) { 83 setState(State.PEEKED); 84 } 85 setSheetTranslation(newSheetViewHeight); 86 } 87 currentSheetViewHeight = newSheetViewHeight; 88 } 89 }; 90 sheetView.addOnLayoutChangeListener(sheetViewOnLayoutChangeListener); 91 }
用于显示和隐藏的API方法。只要是显示就必须调用此方法。
文章判断是否当前状态是隐藏,如果是就显示,否则就隐藏。这样就省去了好多麻烦,如果先显示或者是隐藏调用这一个API就完事。
Line 30 --33 这里这块代码很关键,也许有时候你填充的一个view设置的参数宽度和高度都是match_parent,但是真实的显示确实宽度是macth_parent,但是高度却不是咱们set的那样,问题就在这里了。其实这个判断很简单,先是获取LayooutParams,了解这块的童鞋知道,这里其实获取的是父布局的LayoutParams,但是如果如果你从XML文件inflate的一个view,可想而知,这里其实是没有父布局的,因为你还没有执行为这个view执行addview的操作,所以这里获取的参数肯定为NULL,辣么判断很显然成立,辣么问题来了,这里从新new一个新的params,宽度match_parent,高度wrap_content,然后紧接着50 line完成了addview,然后传入参数就是这个params对象。this is cool....这里动画类的就不说了,我太菜了,看不懂....
前边的GIF大家看了是不是感觉很炫,有个小点不知道大家注意了木,当BottomSheet出现的时候先出现一部分,之后拖动填充整个屏幕?
咱们继续看代码咯,look this.
1 private float getDefaultPeekTranslation() { 2 return hasFullHeightSheet() ? getHeight() / 3 : getSheetView().getHeight(); 3 }
1 private boolean hasFullHeightSheet() { 2 return getSheetView() == null || getSheetView().getHeight() == getHeight(); 3 }
判断你填充的view高度是不是match_parent,为什么只判断高度,因为宽度之前说到了都是macth_parent,这里只要填充的view的高度等于当前父view的高度,那么就先允许你出现当前高度的三分之一。如果不是那就简单咯,有多高就一下展示出来,就这么简单咯。
对于Module bottomsheet的,基本你可能需要知道的就这些了,想要调成布局大小或者是背景透明度的看完上边的就阔以了,下边的就别看了
接下里咱们要说的就是常用的包了。
bottomsheet-commons
Usage:BottomSheetFragment.java BottomSheetFragmentDelegate.java
使用其实很简单,直接创建class 继承这个BootomSheetFragment,然后重写onCreateView方法即可,是不是很简单,调用的时候依然一行code,new 这个新增Fagment对象然后调用期show(FragmentTransaction transaction, @IdRes int bottomSheetLayoutId)方法。
辣么问题来了,方法参数,表示什么意思,他又是如何显示出来的,可能你会说这不是show方法么,其实不然。这里咱们看下源码干了些什么?
1 /** 2 * {@inheritDoc} 3 */ 4 @Override 5 public int show(FragmentTransaction transaction, @IdRes int bottomSheetLayoutId) { 6 return getDelegate().show(transaction, bottomSheetLayoutId); 7 }
1 /** 2 * DialogFragment-like show() method for displaying this the associated sheet fragment 3 * 4 * @param manager FragmentManager instance 5 * @param bottomSheetLayoutId Resource ID of the {@link BottomSheetLayout} 6 */ 7 public void show(FragmentManager manager, @IdRes int bottomSheetLayoutId) { 8 dismissed = false; 9 shownByMe = true; 10 this.bottomSheetLayoutId = bottomSheetLayoutId; 11 manager.beginTransaction() 12 .add(fragment, String.valueOf(bottomSheetLayoutId)) 13 .commit(); 14 }
这就是所有源码中的代码,通过辅助类间接调用,这里很简单就是给FragmentManager添加了一个Fragment对象。额,辣么之前说了一个方法用于显示和隐藏BottomSheet的showWithSheetView方法,这里很显然没调用这个方法。那么跟之前我说的只要显示必须调用showWithSheetView方法不一致了,尼玛,是不是觉得我之吹流弊了,非也,咱们继续看。
其实就翻看源码而言如果你发现遇到上边的问题的时候,你需要想到问题是,Fragment如何和BtoomSheetLayou联系在一起的,这时候你就可以在类
BottomSheetFragment搜索看是不否有对BottomSheetLayout的引用,其次是辅助类,BottomSheetFragmentDelegate,这样你就能很快的定位出其引用关系,按照如上
就定位出在BottomSheetFragmentDelegate中有对BottomSheetLayout的使用,辣么,咱们自然的就想到了是不是有调用显示的方法,辣么代码就来了。
1 /** 2 * Corresponding onStart() method 3 */ 4 public void onStart() { 5 if (bottomSheetLayout != null) { 6 viewDestroyed = false; 7 bottomSheetLayout.showWithSheetView(fragment.getView(), sheetFragmentInterface.getViewTransformer()); 8 bottomSheetLayout.addOnSheetDismissedListener(this); 9 } 10 }
关于其调用来自BottomSheetFragment的onStart方法,代码如下;
1 @Override 2 public void onStart() { 3 super.onStart(); 4 getDelegate().onStart(); 5 }
到了这里大家发现每次Framgent onStart的时候调用,也就完成了显示。这里大家发现所以对BottomSheetlayout的操作都是有辅助类,BottomSheetFragmentDelegate来玩成与BottomSheetLayout的沟通交流。
这里我在唠叨几句我自己感觉挺好的地方。在BottomSheetLayout中重载了一个getLayoutInflater方法,看代码
1 @Override 2 public LayoutInflater getLayoutInflater(Bundle savedInstanceState) { 3 return getDelegate().getLayoutInflater(savedInstanceState, super.getLayoutInflater(savedInstanceState)); 4 }
当然这里也是通过辅助类来完成的操作,这里传入的第二个参数也就是系统自己生成的layoutInflater对象。继续深入下:
1 /** 2 * Retrieves the appropriate layout inflater, either the sheet's or the view's super container. Note that you should 3 * handle the result of this in your getLayoutInflater method. 4 * 5 * @param savedInstanceState Instance state, here to match Fragment API but unused. 6 * @param superInflater The result of the view's inflater, usually the result of super.getLayoutInflater() 7 * @return the layout inflater to use 8 */ 9 @CheckResult 10 public LayoutInflater getLayoutInflater(Bundle savedInstanceState, LayoutInflater superInflater) { 11 if (!showsBottomSheet) { 12 return superInflater; 13 } 14 bottomSheetLayout = getBottomSheetLayout(); 15 if (bottomSheetLayout != null) { 16 return LayoutInflater.from(bottomSheetLayout.getContext()); 17 } 18 return LayoutInflater.from(fragment.getContext()); 19 }
首先判断是不是使用BottomSheetlayout,如果是就直接使用系统生成的layoutInflater对象,否则就是读取bottomSheet对象是不是为null,如果不是就使用bottomSheetlayout的上下文去生成一个layoutInflater对象,否则就直接使用start这个Fragment的Activity的上下文去生成这个layoutInflater对象。
这里咱们深入挖掘下这个BottomSheetLayout对象的获取,
1 /** 2 * @return this fragment sheet's {@link BottomSheetLayout}. 3 */ 4 public BottomSheetLayout getBottomSheetLayout() { 5 if (bottomSheetLayout == null) { 6 bottomSheetLayout = findBottomSheetLayout(); 7 } 8 9 return bottomSheetLayout; 10 }
判断缓存中是否存在这个对象,如果存在就直接return回去这个对象,否则就去查找。
1 @Nullable 2 private BottomSheetLayout findBottomSheetLayout() { 3 Fragment parentFragment = fragment.getParentFragment(); 4 if (parentFragment != null) { 5 View view = parentFragment.getView(); 6 if (view != null) { 7 return (BottomSheetLayout) view.findViewById(bottomSheetLayoutId); 8 } else { 9 return null; 10 } 11 } 12 Activity parentActivity = fragment.getActivity(); 13 if (parentActivity != null) { 14 return (BottomSheetLayout) parentActivity.findViewById(bottomSheetLayoutId); 15 } 16 return null; 17 }
首先判断当前Fragment是否有父Fragment如果当前Fragment没有Attached在一个Activity上返回父Fragemnt对象,否则就返回null。如果不返回null说明此事还没有粘附在任何Activity上,那么久获取onCreateView中的View去查找bottonSheetLayouyId,这个bottonSheetLayouyId在show方法调用的时候传入,然后查找到这个BottomSheetLayout对象。如果反回null,说明此事已经Attach在一个Activity上了,辣么,就回去粘附的Activity对象,也就是上下文对象,然后根据BottomSheetId去查找到这个对象,然后返回给调用者。最后是默认值直接就是null。
基本到这里咱们需要知道有关BottomSheetFragemnt的使用就这些了。
下边就简单commons控件了,大家按照之前我说的很快就能明白了,没啥技术难度的了。。。
就暂时先这些了,文章中的不足之处,请多多指出,以便我及时的修改,谢谢。。。
把想象付诸实践,然后你就成功了一半,人的价值总是在这些成功或失败中得以体现。