自定义通用底部弹框

477 阅读3分钟

项目背景

点击设置按钮,从底部弹出设置选项弹窗,点击里边的选项,能够进行响应。这种底部弹窗是有写好的通用的组件,不过为了练手,后面自己通过继承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布局的使用方式