项目背景
点击设置按钮,从底部弹出设置选项弹窗,点击里边的选项,能够进行响应。这种底部弹窗是有写好的通用的组件,不过为了练手,后面自己通过继承DialogFragment写了一个带有状态监听的通用弹窗 继承DialogFragment,重写onCreateView和onViewCreated方法
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
final BottomSheetParams params = ofDelegate().getParams();
return inflater.inflate(
params.mIsSoftInputEnabled && params.mContainerLayout == R.layout.bottom_sheet_container
? R.layout.bottom_sheet_container : params.mContainerLayout, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final BottomSheetDelegate delegate = ofDelegate();
delegate.onViewCreated(view);
delegate.mDestroyContainerRunnable = super::dismissAllowingStateLoss;
delegate.mBackPressableConsumer = enabled ->{
if(getDialog()==null)return;
if(enabled){
if(mOnKeyListener == null){
mOnKeyListener = (v,keyCode,event)->{
if(keyCode== KeyEvent.KEYCODE_BACK){
if(delegate.mState.isPanelExpanded()){
delegate.mState.hidePanel();
return true;
}
}
return false;
};
}
getDialog().setOnKeyListener(mOnKeyListener);
}else {
getDialog().setOnKeyListener(null);
}
};
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/bs_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:importantForAccessibility="no"
android:soundEffectsEnabled="false"
tools:background="@color/p_color_black_alpha30" />
<FrameLayout
android:id="@+id/bs_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/bottom_sheet_behavior"
tools:layout_height="250dp"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
这里采用了代理模式思想,隐藏了ContentView的创建细节,以及内部弹窗收起和打开的状态监听和变换
@NonNull
BottomSheetDelegate ofDelegate() {
if (mDelegate == null) {
mDelegate = new BottomSheetDelegate(this);
}
return mDelegate;
}
在面板dissmiss和destroy的时候,通过代理类去管理相关资源的释放
@Override
public void dismiss() {
ofDelegate().mState.hidePanel();
}
@Override
public void dismissAllowingStateLoss() {
ofDelegate().mState.hidePanel();
}
@Override
public void onDestroyView() {
super.onDestroyView();
ofDelegate().onDestroyView();
}
另外可以在onActivityCreated中对Window的样式进行设置
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final Window window = getDialog()!=null?getDialog().getWindow():null;
//设置导航栏为沉浸式模式
if (window != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//一些手机状态栏背景色是半透明的黑色,下面三行代码可以看到全透明的状态栏
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
}
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
window.setWindowAnimations(0);
window.setDimAmount(0);
//对软键盘的处理
if (ofDelegate().getParams().mIsSoftInputEnabled) {
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager
.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
}
}
if (getView() != null && getView().getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) getView().getLayoutParams();
mlp.bottomMargin = ofDelegate().getParams().mBottomMargin;
}
}
主要逻辑在代理类中,在代理类中主要做了如下几个事情:
1、将根部局中的Framelayout替换成用户自己的Fragment样式
final Fragment content;
try {
content = host.getChildFragmentManager().getFragmentFactory()
.instantiate(view.getContext().getClassLoader(), contentName);
content.setArguments(contentArgs != Bundle.EMPTY ? contentArgs : new Bundle());
} catch (Throwable e) {
mState.hidePanel();
return false;
}
host.getChildFragmentManager().beginTransaction()
.replace(R.id.bs_bottom_sheet, content, mContentTag)
.runOnCommit(
() -> {
addToAutoDisposable(Single
.timer(50, TimeUnit.MILLISECONDS, Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(any -> mState.showPanel(), e -> {
mState.hidePanel();
})
);
}
)//当时该事务提交完之后过50ms,通过PublishSubject发起面板打开的通知
.commitAllowingStateLoss();
2、面板是否即将打开的监听
mState.observeShowPanel()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(showPanle -> {
if (showPanle) {//打开面板
if (mBehavior.getState() != BottomSheetBehavior.STATE_EXPANDED) {
reflectAnimDuration(view.getContext());//这里利用反射修改了面板滚出的duration
mBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);//mBehavior是谷歌提供的
com.google.android.material.bottomsheet.BottomSheetBehavior
}
setContainerVisible(view,true);//将View显示,表示打开面板
stopSurviveTimer();
} else {//关闭面板
if (mBehavior.getState() != BottomSheetBehavior.STATE_HIDDEN) {
mBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
}
})
3、为mBehavior设置回调监听,用于在面板弹出的时候给dialog设置OnKeyListener以及按下back键后发送面板隐藏的消息
mBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
mState.notifyPanelHidden();
} else if (newState == BottomSheetBehavior.STATE_EXPANDED) {
mState.notifyPanelExpanded();
}
}
delegate.mBackPressableConsumer = enabled ->{
if(getDialog()==null)return;
if(enabled){
if(mOnKeyListener == null){
mOnKeyListener = (v,keyCode,event)->{
if(keyCode== KeyEvent.KEYCODE_BACK){
if(delegate.mState.isPanelExpanded()){
delegate.mState.hidePanel();
return true;
}
}
return false;
};
}
getDialog().setOnKeyListener(mOnKeyListener);
}else {
getDialog().setOnKeyListener(null);
}
};
4、当面板隐藏的时候延时销毁资源,防止内存泄漏
private void startSurviveTimer() {
if (mHost.isDetached()) {
return;
}
final long surviveTimeMs = getParams().mSurviveTimeMs;
if (surviveTimeMs <= 0) {
return;
}
if(mSurviveDisposable!=null){
mSurviveDisposable.dispose();
}
mSurviveDisposable = Single
.timer(surviveTimeMs, TimeUnit.MILLISECONDS, Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(any -> {
destroyContent();
}, e -> {
executeDestroyContainerRunnable();
});
}
5、通过反射修改面板滚出的时间(用户可自定义)
private void reflectAnimDuration(Context context) {
try{
Field field = BottomSheetBehavior.class.getDeclaredField("viewDragHelper");//反射
field.setAccessible(true);
ViewDragHelper mHelper = (ViewDragHelper) field.get(mBehavior);//拿到mBehavior中的mHelper对象
Field scrollerFiled = ViewDragHelper.class.getDeclaredField("mScroller");
scrollerFiled.setAccessible(true);
//修改系统弹出面板的时间
scrollerFiled.set(mHelper,new BottomSheetOverScroll(context,getParams().mExpandAnimDuration));
}catch (Exception e){
}
}
public class BottomSheetOverScroll extends OverScroller {
private final int mHookDuration;
public BottomSheetOverScroll(Context context,int hookDuration) {
super(context);
mHookDuration = hookDuration;
}
@Override
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
if (mHookDuration > 0) {
duration = mHookDuration;
}
super.startScroll(startX, startY, dx, dy, duration);
}
}
总结:
通过该项目,学会使用了PublishSubject的使用方法,用以面板展示隐藏的收发通知; 同时锻炼了阅读源码的能力,通过反射技术修改系统工具类相关属性值 学习了CoordinatorLayout布局的使用方式