Flipboard的bottomsheet源码地址https://github.com/Flipboard/bottomsheet。
bottomsheet中最重要的是BottomSheetLayout,其实它是个自定义的ViewGroup,继承于FrameLayout。查看文档和bottomsheet-sample里的例子,调用入口都是showWithSheetView方法,如PickerActivity中使用
bottomSheetLayout.showWithSheetView(intentPickerSheet);
intentPickerSheet就是从底部弹出的View,在Module bottomsheet-commons中,Flipboard给了几个封装好的View,适用于相关场景,比如选择图片,选择Menu等。本文重点是分析BottomSheetLayout。
在从入口方法showWithSheetView分析前,先说明两个问题,一个是BottomSheetLayout的状态,另一个是BottomSheetLayout的布局结构。
一、BottomSheetLayout状态说明
BottomSheetLayout根据弹窗的状态,分为四个状态:
public enum State {
HIDDEN,\\隐藏
PREPARING,\\准备中,唯一出现时机就是调用showWithSheetView开始时
PEEKED,\\弹出
EXPANDED\\见下面的说明
}
(在BottomSheetLayout中,弹出的View定义为sheetView,BottomSheetLayout会定义一个默认的弹出高度peekKeyline,如果弹出的sheetView高度大于peekKeyline,并且显示的高度大于peekKeyline,则State为EXPANDED。)
peekKeyline的定义如下:
peekKeyline = point.y - (screenWidth / (16.0f / 9.0f));\\point.y为屏幕的高度
二、BottomSheetLayout布局结构
1.BottomSheetLayout是根布局,布局文件中BottomSheetLayout包含的View为第一个childView
2.dimView是BottomSheetLayout的第二个childView,dimView就是那个黑色半透明背景,代码定义就是 dimView = new View(getContext());定义透明而已
3.sheetView在最上面,是BottomSheetLayout的第三个childView,sheetView就是弹出的View,上面说了
那么问题来了,dimView和sheetView是怎么加入到BottomSheetLayout的?
从源头看,在Activity的onCreate中调用setContentView,然后inflate布局文件,BottomSheetLayout是个ViewGroup,inflate的时候调用
viewGroup.addView(view, params);
BottomSheetLayout重写了addView方法,
@Override
public void addView(@NonNull View child) {
if (getChildCount() > 0) {
throw new IllegalArgumentException("You may not declare more
then one child of bottom sheet. The sheet view must be added
dynamically with showWithSheetView()");
}
setContentView(child);
}
public void setContentView(View contentView) {
super.addView(contentView, -1, generateDefaultLayoutParams());\\第一个childView
super.addView(dimView, -1, generateDefaultLayoutParams());\\第二个childView
}
dimView被add了,那sheetView是什么时候add呢?答案在showWithSheetView里。showWithSheetView里有这样一句
super.addView(sheetView, -1, params);
这样之后的代码中获得sheetView的方法就容易看懂了
public View getSheetView() {
return getChildCount() > 2 ? getChildAt(2) : null;\\直接取第三个childView
}
下面进入正题,开始分析showWithSheetView(与isTablet相关的代码可以不用管,我们主要是针对手机平台,而不是平板电脑)。
三、showWithSheetView流程分析
代码太长,有些没贴,最好对照源码看。
showWithSheetView有几个重载方法,最终走到
public void showWithSheetView(final View sheetView, final ViewTransformer viewTransformer) ,
如果用户没有提供自定义的ViewTransformer,BottomSheetLayout就使用默认的BaseViewTransformer,功能很简单,就是根据弹出高度计算dimView的透明度,代码不贴了,看看就知道了。
进入showWithSheetView方法,首先走到
if(state!=State.HIDDEN){
Runnable runAfterDismissThis=new Runnable(){
@Override
public void run(){
showWithSheetView(sheetView,viewTransformer);
}
};
dismissSheet(runAfterDismissThis);
return;
}
在调用showWithSheetView前,BottomSheetLayout可能不是HIDDEN状态,这段代码就是处理这种情况,看名字runAfterDismissThis就是知道,先调用dismissSheet隐藏sheetView,在dismissSheet动画结束时,再调用runAfterDismissThis,相关代码在dismissSheet里监听动画结束的地方onAnimationEnd
setState(State.HIDDEN);\\改变状态为HIDDEN
setSheetLayerTypeIfEnabled(LAYER_TYPE_NONE);\\关闭硬件加速
removeView(sheetView);\\隐藏动画结束sheetView被remove了
if (runAfterDismiss != null) {
runAfterDismiss.run();
runAfterDismiss = null;
}
然后是设置BottomSheetLayout状态是PREPARING,接着一段是设置sheetView的LayoutParams,然后调用addView把sheetView加入到BottomSheetLayout,之前说BottomSheetLayout布局结构时说过,当然最开始sheetView位置设置到了BottomSheetLayout的底部不可见,通过initializeSheetValues方法中的
getSheetView().setTranslationY(getHeight());
位置都设置好了,开始动画,之所以post方式开始动画,是要sheetView已经画过在执行peekSheet。
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(this);
post(new Runnable() {
@Override
public void run() {
// Make sure sheet view is still here when first draw happens.
// 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.
if (getSheetView() != null) {
peekSheet();
}
}
});
return true;
}
上面一段,主要是走到peekSheet方法,开始动画。最下面的sheetViewOnLayoutChangeListener先不用管。
peekSheet代码如下:
public void peekSheet() {
cancelCurrentAnimation();
setSheetLayerTypeIfEnabled(LAYER_TYPE_HARDWARE);
ObjectAnimator anim = ObjectAnimator.ofFloat(this, SHEET_TRANSLATION, getPeekSheetTranslation());
anim.setDuration(ANIMATION_DURATION);
anim.setInterpolator(animationInterpolator);
anim.addListener(new CancelDetectionAnimationListener() {
@Override
public void onAnimationEnd(@NonNull Animator animation) {
if (!canceled) {
currentAnimator = null;
}
}
});
anim.start();
currentAnimator = anim;
setState(State.PEEKED);
}
peekSheet的功能就是启用硬件加速在sheetView上画属性动画,设置BottomSheetLayout的状态为PEEKED。当然这里有个属性动画怎么画的问题,wrap了一层,可以参考代码中的SHEET_TRANSLATION,这里不分析了。
总结一下,showWithSheetView方法主要功能就是add sheetView到BottomSheetLayout后开始弹出动画。
以后再分析BottomSheetLayout比较有亮点的功能,可拖动sheetView。