由 Widget 理念到 Dialog 的模拟实现

Android 诞生起,Dialog就跟随着用户的使用习惯,不停的改变样式,从开始的Dialog到之后 DialogFragment,不仅仅是外观的变化;

Dialog 自身是无法进行显示的,底层借助了 View 以及 Window ,才能和用户进行交互,不过由于应用重启时,会伴随着 Context 对象的创建与销毁,此时 还在显示的 Dialog 会因为原寄主对象不存在而导致应用崩溃。

之后为了解决此类问题,官方提供了 DialogFragment,原理在于:让dialog的显示与隐藏与一个fragment的声明周期进行绑定,当Activity 由于屏幕旋转等原因销毁时,通过 fragment 来控制dialog先行关闭,由此来避免崩溃等问题。


虽然说DialogFragment “任务” 完成的不错,但 Dialog 与 DialogFragment 需要编写大量的代码,用于Activity 与 Dialog 进行数据交互;即便是采用了 Builder设计模式,依然摆脱不了繁琐的事实;

不过幸好,还有其他方法可以完成 Dialog 的工作。

1、 Flutter for Android

近期出现的Flutter可谓是一剂良方,创建一个项目然后打开其源码,会发现 如下代码:

public class MainActivity extends FlutterActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
  }
}

一般而言,我们都是在 onCreate方法中添加 layout布局 ,然后由系统自动进行加载显示的。那么Flutter在这里做了什么?

跟随源码继续进入:

public class FlutterActivity extends Activity implements Provider, PluginRegistry, ViewFactory {
    //....
    private final FlutterActivityDelegate delegate = new FlutterActivityDelegate(this, this);
    private final FlutterActivityEvents eventDelegate;
    //....

	public FlutterActivity() {
        this.eventDelegate = this.delegate;
        this.viewProvider = this.delegate;
        // ...
    }
    
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.eventDelegate.onCreate(savedInstanceState);
    }
}

这里出现的 eventDelegate 其实是一个代理,每当Activity 到了某个生命周期时,会调用该代理类对应的生命周期方法。

在构造方法中,可以看到其实 eventDelegate 是 FlutterActivityDelegate 类型,那就继续进入 FlutterActivityDelegate 中,查看 onCreate 时,做了什么:

public final class FlutterActivityDelegate implements FlutterActivityEvents, Provider, PluginRegistry {

		//...
		
        public void onCreate(Bundle savedInstanceState) {
        if (VERSION.SDK_INT >= 21) {
            // ...
        }

        // ...
        if (this.flutterView == null) {
            FlutterNativeView nativeView = this.viewFactory.createFlutterNativeView();
            this.flutterView = new FlutterView(this.activity, (AttributeSet)null, nativeView);
            this.flutterView.setLayoutParams(matchParent);
            
            // 着重这一行
            this.activity.setContentView(this.flutterView);
            
            // ...
        }

        // ...
    }

	// ...
}

其他部分 我们先不考虑,只看中间标注的一行,这就说明:Activity 中,只加载 了一个 View——flutterView

flutterView 是什么?

public class FlutterView extends SurfaceView implements BinaryMessenger, TextureRegistry {
	// ...
}

在结合 Flutter 官方给出的一个图表:

在这里插入图片描述

我们有理由相信,其实在Android端,Flutter 通过一个可以自己进行绘制逻辑的 SurfaceView 完成了整体的构建。

2、 Cordova for Android

看完了Flutter,接下来借用一个 Cordova 项目来看一下其实现原理;

同样的,我们查看应用的第一个 Activity

public class MainActivity extends CordovaActivity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        // enable Cordova apps to be started in the background
        Bundle extras = getIntent().getExtras();
        if (extras != null && extras.getBoolean("cdvStartInBackground", false)) {
            moveTaskToBack(true);
        }

        // Set by <content src="index.html" /> in config.xml
        loadUrl(launchUrl);
    }
}

然后进入 loadUrl 方法中:

public class CordovaActivity extends Activity {
	// ...
    public void loadUrl(String url) {
        if (appView == null) {
            init();
        }

        // If keepRunning
        this.keepRunning = preferences.getBoolean("KeepRunning", true);

        appView.loadUrlIntoView(url, true);
    }
    
	// ...
}

接着是进入 init 方法:

public class CordovaActivity extends Activity {
	// ...

    protected void init() {
        appView = makeWebView();
        createViews();
        if (!appView.isInitialized()) {
            appView.init(cordovaInterface, pluginEntries, preferences);
        }
        cordovaInterface.onCordovaInit(appView.getPluginManager());

        // Wire the hardware volume controls to control media if desired.
        String volumePref = preferences.getString("DefaultVolumeStream", "");
        if ("media".equals(volumePref.toLowerCase(Locale.ENGLISH))) {
            setVolumeControlStream(AudioManager.STREAM_MUSIC);
        }
    }
    
	// ...
}

这时,通过 makeWebView 方法,创建了一个 CordovaWebView 的实例,然后在 createViews 方法中:

 protected void createViews() {
        //Why are we setting a constant as the ID? This should be investigated
        appView.getView().setId(100);
        appView.getView().setLayoutParams(new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT));

        setContentView(appView.getView());

        // ...
    }

ok,到这就可以看懂,其实 Cordova 项目对于安卓来说 ,也是放入了一个布局:appView.getView()
该方法层层跟踪,其实可以看到是这样的:

public class CordovaWebViewImpl implements CordovaWebView {
   public static CordovaWebViewEngine createEngine(Context context, CordovaPreferences preferences) {
        String className = preferences.getString("webview", SystemWebViewEngine.class.getCanonicalName());
        try {
            Class<?> webViewClass = Class.forName(className);
            Constructor<?> constructor = webViewClass.getConstructor(Context.class, CordovaPreferences.class);
            return (CordovaWebViewEngine) constructor.newInstance(context, preferences);
        } catch (Exception e) {
            throw new RuntimeException("Failed to create webview. ", e);
        }
    }
}

最终创建过程在 SystemWebView 类中

即,通过反射最后生成了一个 WebView 的包装类,然后获取到 WebView 后,添加到了Activity中,接着所有的界面通过网页进行加载和显示。


通过对 Flutter 项目 和 Cordova 的观察,我们不禁想,是否可以利用这样的方法,模拟出 dialog 来,而不用每次都去创建 dialog (DialogFragment形式同样需要先创建出 dialog)?

使用WebView当然不行,那样逻辑更复杂了;

使用SurfaceView 呢?当然还是不行,如果要自己一点一点绘制出dialog来,着实没有必要。

那么最后,只能去考虑普通的 View 类了,事实上,Dialog 本身用于显示的部分,也只是其中的 contentView 而已。


3、模拟 dialog

用 view 来模拟 dialog,其实跟平时写布局文件一样,只是为了更形象一些,在根部局尽量使用 CardView 来设置一定z轴高度以及弧度。例如这样:

dialog_first.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        app:cardBackgroundColor="#ee654321"
        app:cardCornerRadius="5dp"
        app:cardElevation="@dimen/cardview_default_elevation"
        android:layout_marginStart="48dp"
        android:layout_marginEnd="48dp">

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="vertical"
            android:padding="16dp">

        <TextView
                android:textColor="#FFFFFF"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:text="我是标题:点击确认,弹窗不会消失"/>

        <Button
                android:id="@+id/bt_submit"
                android:layout_width="wrap_content"
                android:layout_height="48dp"
                android:layout_marginTop="32dp"
                android:gravity="center"
                android:text="确认"/>
    </LinearLayout>
</android.support.v7.widget.CardView>

样式大概 是这样的:

在这里插入图片描述

单就样式而言,与一般的 dialog 已经没有太大的区别了。

接下来我们考虑怎么控制这个 view 在界面中进行显示:

能否把该 布局 includeactivity 的布局中,然后通过 visibility 属性控制显示与隐藏?

理论上当然可以,每次弹出时候,屏蔽掉其他所有控件的点击等事件,不过这样一来,控件的显示和隐藏逻辑会很复杂,还不如使用 Dialog

使用 include 方式最大的短板,还是代码量过大,逻辑太复杂,如果可以有一种方式,在编写dialog 完成后,可以直接进行显示或者隐藏就好了,那样代码量会减少很多。

控制视图显示或者隐藏,最好的布局就是 FrameLayout,因为它比较简单,自身布局的显示和隐藏不会对其他布局产生很大影响;因此我们可以找一个FrameLayout,然后由FrameLayout来控制创建好的 dialog。

事实上,根据安卓的布局结构来说,已经自带了FrameLayout布局:

有 toolbar 的界面布局 大概这个样子:

在这里插入图片描述

(不考虑中间遮挡到的许多按钮)把这个布局展开,会是这样的:

<?xml version="1.0" encoding="utf-8"?>
<!--手机屏幕分辨率:720*1280-->
<!--根部局:FrameLayout子类-->
<com.android.internal.policy.DecorView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="720px"
    android:layout_height="1280px">

    <!--包含内容布局,以及statusBar-->
    <LinearLayout
        android:layout_width="720px"
        android:layout_height="1280px">

        <!--view-stub 加载状态栏图标等信息,加载完成后,高度为48px,内容加载前为0px-->
        <ViewStub
            android:id="@+id/action_mode_bar_stub"
            android:layout_width="720px"
            android:layout_height="48px" />


        <!--内容布局:包括导航栏action-bar 以及内容布局-->
        <FrameLayout
            android:layout_width="720px"
            android:layout_height="1232px">

            <!--包含 actionbar 和 content-->
            <android.support.v7.widget.ActionBarOverlayLayout
                android:id="@+id/decor_content_parent"
                android:layout_width="720px"
                android:layout_height="1232px">

                <!--actionbar-->
                <android.support.v7.widget.ActionBarContainer
                    android:id="@+id/action_bar_container"
                    android:layout_width="720px"
                    android:layout_height="112px">
                    
                    <!--toolbar-->
                    <android.support.v7.widget.Toolbar
                        android:id="@+id/action_bar"
                        android:layout_width="720px"
                        android:layout_height="112px">
                        
                        <!--toolbar 部分内容-->
                        
                    </android.support.v7.widget.Toolbar>
                    
                    <!--ActionBarContextView 无内容-->
                    <android.support.v7.widget.ActionBarContextView
                        android:id="@+id/action_context_bar"
                        android:layout_width="0px"
                        android:layout_height="0px">
                    </android.support.v7.widget.ActionBarContextView>
                    
                </android.support.v7.widget.ActionBarContainer>
                
                <!--主内容布局 :content-->
                <android.support.v7.widget.ContentFrameLayout
	                android:id="@+id/content"
                    android:layout_width="720px"
                    android:layout_height="1120px">
                    
                    <!--自定定义的Activity中的布局内容-->
                    
                </android.support.v7.widget.ContentFrameLayout>
                
            </android.support.v7.widget.ActionBarOverlayLayout>

        </FrameLayout>

    </LinearLayout>

	 <!--状态栏颜色,处于最底部-->
    <View
        android:id="@+id/statusBarBackground"
        android:layout_width="720px"
        android:layout_height="48px"/>
</com.android.internal.policy.DecorView>

也就是说,我们在开始布局时,已经有了五层 parent存在。

全屏状态 的界面布局 大概这个样子:

在这里插入图片描述

同样的布局文件类似如此:

<?xml version="1.0" encoding="utf-8"?>
<!--手机屏幕分辨率:720*1280-->
<!--根部局:FrameLayout子类-->
<com.android.internal.policy.DecorView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="720px"
    android:layout_height="1280px">

    <!--包含内容布局,以及statusBar-->
    <LinearLayout
        android:layout_width="720px"
        android:layout_height="1280px">

        <!--view-stub 加载状态栏图标等信息,因 全屏,所以高度为0-->
        <ViewStub
            android:id="@+id/action_mode_bar_stub"
            android:layout_width="720px"
            android:layout_height="0px" />

        <!--内容布局:包括导航栏action-bar 以及内容布局-->
        <FrameLayout
            android:layout_width="720px"
            android:layout_height="1280px">

            <!--包含 actionbar 和 content-->
            <android.support.v7.widget.FitWindowsLinearLayout
                android:id="@+id/action_bar_root"
                android:layout_width="720px"
                android:layout_height="1280px">

                <!--没有了 action-bar;出现了两个statubar的位置-->
                <android.support.v7.widget.ViewStubCompat
                    android:id="@+id/action_mode_bar_stub"
                    android:layout_width="0px"
                    android:layout_height="0px">

                </android.support.v7.widget.ViewStubCompat>

                <!--主内容布局 :content-->
                <android.support.v7.widget.ContentFrameLayout
                    android:id="@+id/content"
                    android:layout_width="720px"
                    android:layout_height="1280px">

                    <!--自定定义的Activity中的布局内容-->

                </android.support.v7.widget.ContentFrameLayout>

            </android.support.v7.widget.FitWindowsLinearLayout>

        </FrameLayout>

    </LinearLayout>

</com.android.internal.policy.DecorView>

没有 toolbar 的界面布局 大概这个样子(与全屏状态相比,多了状态栏):

在这里插入图片描述

布局文件类似为:

<?xml version="1.0" encoding="utf-8"?>
<!--手机屏幕分辨率:720*1280-->
<!--根部局:FrameLayout子类-->
<com.android.internal.policy.DecorView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="720px"
    android:layout_height="1280px">

    <!--包含内容布局,以及statusBar-->
    <LinearLayout
        android:layout_width="720px"
        android:layout_height="1280px">

        <!--view-stub 加载状态栏图标等信息,加载完成后,高度为48px,内容加载前为0px-->
        <ViewStub
            android:id="@+id/action_mode_bar_stub"
            android:layout_width="720px"
            android:layout_height="48px" />

        <!--内容布局:包括导航栏action-bar 以及内容布局,内容距离顶部有48px间隔-->
        <FrameLayout
            android:layout_width="720px"
            android:layout_height="1232px">

            <!--包含 actionbar 和 content-->
            <android.support.v7.widget.FitWindowsLinearLayout
                android:id="@+id/action_bar_root"
                android:layout_width="720px"
                android:layout_height="1232px">

                <!--没有了 action-bar;出现了两个statubar的位置-->
                <android.support.v7.widget.ViewStubCompat
                    android:id="@+id/action_mode_bar_stub"
                    android:layout_width="0px"
                    android:layout_height="0px">

                </android.support.v7.widget.ViewStubCompat>

                <!--主内容布局 :content-->
                <android.support.v7.widget.ContentFrameLayout
                    android:id="@+id/content"
                    android:layout_width="720px"
                    android:layout_height="1232px">

                    <!--自定定义的Activity中的布局内容-->

                </android.support.v7.widget.ContentFrameLayout>

            </android.support.v7.widget.FitWindowsLinearLayout>

        </FrameLayout>

    </LinearLayout>

 	<!--状态栏颜色,处于最底部-->
    <View
        android:id="@+id/statusBarBackground"
        android:layout_width="720px"
        android:layout_height="48px"/>
</com.android.internal.policy.DecorView>

附 : 控制上面显示效果的 style属性包括如下

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>

        <!--样式效果属性-->
        
        <!--全屏效果-->
        <item name="android:windowFullscreen">true</item>

        <!--无actionBar,无 title-->
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>

        <!--状态栏透明化,且不占用内容部分,不过此模式时,windowFullscreen属性需要为false-->
        <!--android:windowTranslucentStatus模式下,布局样式和 全屏模式类似,因此此时 statusBar处于悬浮状态,不占用布局部分(可以理解为单独的window窗口)-->
        <item name="android:windowTranslucentStatus">true</item>
    </style>
</resources>

总结来看,即便界面没有任何内容,父布局层级中也会有两个 FrameLayout布局(实际上DecorView 也是FrameLayout子类,但由于该类不可见 hide模式 ,所以这里先不考虑),其中第一个布局没有id,因此不好获取,第二个布局就是 我们经常说的content布局了。

  1. 普通模式下(有 statusbar,有 toolbar):content 高度为 1120 px,状态栏48px,toolbar 112 px
  2. 简洁模式下(有 statusbar,无 toolbar):content 高度为 1232 px,其中状态栏占用了 48 px
  3. 全屏模式下(无 statusbar,无 toolbar):content 高度为 1280 px,占用全屏,windowTranslucentStatus模式与全屏模式相同

可见看到,几种模式下,布局的大小不尽相同,而我们要做的,是要针对所有的情况,都可以使用同一种方式去显示dialog。

那么我们使用 DecorView 是否可行?
这个问题留待下次再说,我们目前仅以一种简单的方式来实现此种逻辑。


上面提到,要统一的进行弹窗,需要用到一个 FrameLayout 布局,充当 dialog 容器,既然无法找到一个适配所有情况的容器,那么不如干脆更简单的一些,我们不再使用系统已有的布局,现要求用户必须在根部局处放置一个我们自定义的 FrameLayout 布局,该布局中我们修改了部分逻辑,然后定义一些通用的方法:

那么现在来大概猜想一下,此FrameLayout布局大致需要包含的功能:

  1. 可以显示 dialog,可以显示多个dialog
  2. 可以隐藏 dialog,可以隐藏多个dialog
  3. 需要为新创建的 dialog 提供注册接口 ,否则此 dialog容器无法获知有哪些 dialog 存在
  4. 提供注销接口,为不再需要弹出显示 dialog 保留移除功能
  5. 判断当前是否有 dialog 在显示,为主activity中必要的逻辑做准备
  6. 最后,为用户保留自定义动画的接口,以实现动态美观的效果

看起来可能功能比较 麻烦,但其实都 很简单,逻辑很清晰,按照每个逻辑一个方法来设计,大概我们做如下功能:IncludeDialogViewGroup 源码

class IncludeDialogViewGroup : FrameLayout, View.OnClickListener {
    /**
     * 记录当前装载的dialog
     */
    private var allDialogs: MutableList<SimulateDialogInterface<*, *>> = mutableListOf()

    /**
     * 装载 dialog 对应的 animator对象,可执行动画
     */
    private var dialogAnimators: HashMap<SimulateDialogInterface<*, *>, ProvideIncludeDialogVGAnimator> = hashMapOf()

    /**
     * 遮罩布局
     */
    private var shadeView = FrameLayout(context)

    /**
     * dimen颜色(遮罩颜色值)
     */
    var maskColor: Int = defaultMaskColor

    /**
     * 当点击 dialog "外部" 时候,是否关闭弹框
     */
    var closeOnClickOut: Boolean = defaultCloseOnClickOut

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(
        context,
        attrs,
        defStyleAttr,
        defStyleRes
    )

    /**
     * 布局加载完成后,再进行事件或其他逻辑(此时务必保证Activity的布局已经进行了加载)
     */
    @CallSuper
    override fun onFinishInflate() {
        super.onFinishInflate()

        //当布局结束后,标记到context,当前界面有可能弹出dialog
        getActivityFromView().findViewById<View>(android.R.id.content).setTag(R.id.id_include_dialog_view_group, this)

        //同时添加一个中间的布局View,用作遮挡板等功效
        addView(shadeView, LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
    }

    /**
     * 如果处在处于显示状态的view
     */
    fun existDialogOpened(): Boolean {
        //避免多个线程调用导致出现异常
        synchronized(this) {
            return allDialogs.any { it.generateView().parent != null }
        }
    }

    /**
     * 显示某个/些dialog,默认不显示所有
     *
     * 通过instance或者class,或者可以显示所有
     *
     * [instance] dialog 对象引用
     * [clazz] 显示class 为 clazz 的dialog
     * [showAll] true 表示显示所有被注册的dialog
     * [animator] 显示/隐藏 dialog 时的动画逻辑,空表示不执行动画
     */
    fun showDialogs(
        instance: SimulateDialogInterface<*, *>? = null,
        clazz: Class<SimulateDialogInterface<*, *>>? = null,
        showAll: Boolean = false,
        animator: ProvideIncludeDialogVGAnimator? = null
    ) {
        allDialogs.asSequence()
            .filter {
                showAll
                        || it === instance
                        || clazz?.isAssignableFrom(it::class.java) ?: false
            }.map {
                //第一,添加布局,使用动画效果
                if (animator != null) {
                    //保存动画对象
                    dialogAnimators[it] = animator

                    //调用方法开启动画
                    animator.startAnimator(this@IncludeDialogViewGroup, it) {
                        addView(it.generateView(), it.generateLayoutParams())
                    }
                } else {
                    //移除之前的动画对象
                    dialogAnimators.remove(it)

                    //无动画添加 view 进行显示
                    addView(it.generateView(), it.generateLayoutParams())
                }
            }
            .toList()
            .filterAlso({ it.isNotEmpty() }) {
                //第二,添加dimenColor
                shadeView.setBackgroundColor(maskColor)

                //第三,添加点击事件
                shadeView.setOnClickListener(this)
            }
    }


    /**
     * 关闭已经打开的布局(必须制定布局,或者关闭所有,默认关闭所有)
     *
     * 1.关闭被模拟的布局
     * 2.修改自身的状态信息,包括被屏蔽的点击事件,以及背景颜色等等
     *
     * [instance] dialog 对象引用
     * [clazz] 显示class 为 clazz 的dialog
     * [closeAll] true 表示显示所有被注册的dialog
     * [useAnimator] 是否使用动画效果来关闭,当为true且之前开启动画时,即[showDialogs]方法的 [showDialogs#animator)]
     *
     * @return  返回被关闭的dialog数量
     */
    fun closeDialogsOpened(
        instance: SimulateDialogInterface<*, *>? = null,
        clazz: Class<SimulateDialogInterface<*, *>>? = null,
        closeAll: Boolean = true,
        useAnimator: Boolean = true
    ): Int {
        val removeMask = {
            //判断,如果当前所有dialog都已关闭,则修改颜色值,取消遮罩监听
            if (!existDialogOpened()) {
                //第二,移除背景dimenColor
                shadeView.setBackgroundColor(Color.TRANSPARENT)

                //第三,移除监听事件
                shadeView.setOnClickListener(null)
                shadeView.isClickable = false
            }
        }

        return allDialogs.filter {
            closeAll
                    || it === instance
                    || clazz?.isAssignableFrom(it::class.java) ?: false
        }.filter {
            (it.generateView().parent != null).onTrue {
                //第一,移除布局
                if (useAnimator && dialogAnimators.containsKey(it)) {
                    //是否使用动画
                    dialogAnimators[it]!!.stopAnimator(this@IncludeDialogViewGroup, it) {
                        removeView(it.generateView())
                        removeMask()
                    }
                } else {
                    removeView(it.generateView())
                    removeMask()
                }
            }
        }.size
    }

    /**
     * 注册 dialog
     *
     * [dialog] 用于操作的view
     */
    fun registerDialog(dialog: SimulateDialogInterface<*, *>) {
        if (allDialogs.indexOf(dialog) == -1) {
            allDialogs.add(dialog)

            //对dialog人为添加点击事件,防止点击dialog时,触发shadeView布局导致弹窗关闭
            dialog.generateView().setOnClickListener {
                //不处理任何时间,只是防止点击dialog时被关闭
            }
        }
    }

    /**
     * 注销 某个dialog ,注销后,将移除,不是关闭,默认注销所有dialog
     *
     * [instance] dialog 引用
     * [clazz] 通过类型移除
     * [logoutAll]  移除所有
     *
     * @return 移除了多少个dialog
     */
    fun logoutDialogs(
        instance: SimulateDialogInterface<*, *>? = null,
        clazz: Class<SimulateDialogInterface<*, *>>? = null,
        logoutAll: Boolean = true
    ): Int {
        return allDialogs.asSequence()
            .filter {
                logoutAll
                        || it === instance
                        || clazz?.isAssignableFrom(it::class.java) ?: false
            }.map {
                allDialogs.remove(it)
            }.toList().size
    }

    /**
     * Called when a view has been clicked.
     *
     * @param v The view that was clicked.
     */
    override fun onClick(v: View?) {
        //当dialog可见时,该点击事件用来防止误点到布局底部的视图
        if (existDialogOpened() && closeOnClickOut) {
            closeDialogsOpened()
        }
    }

    /**
     * 禁止自身设置点击事件
     */
    @Deprecated(message = "not support", replaceWith = ReplaceWith("super.setOnClickListener"))
    override fun setOnClickListener(l: OnClickListener?) {
        TODO()
    }

    /**
     * 默认伴生类,放置静态的 配置选项
     */
    companion object {
        /**
         * 默认的遮罩颜色
         */
        var defaultMaskColor: Int = Color.parseColor("#40000000")

        /**
         * 点击 外部时,是否默认的关闭弹出框
         */
        var defaultCloseOnClickOut: Boolean = true
    }
}

其实这段代码,除了实现上述基本 功能外,还多了一些逻辑,包括:

  1. 提供 shadeView 遮罩,在模拟的 dialog 弹出时,可以模拟一层覆盖物,使显示效果更接近 原生 dialog。
  2. 在 onFinishInflate 方法中,我们将自身引用给到系统的 content 布局,这样在使用时,就不再需要显示 的为 该容器指定 id 属性。

我们 还规定了被模拟的 dialog 必须实现 SimulateDialogInterface接口,通过该接口,dialog 容器(即IncludeDialogViewGroup)可以获取需要显示的 View ,以及 添加到父布局的 LayoutParams 属性;
注意:generateView 方法多次调用必须返回同一个 View 对象

/**
 * Created on 2019/3/23  20:37
 * function : 模拟dialog的控件,必须具有以下功能:
 * <p>
 * 1.获取加载到ViewGroup中的layoutParams方式
 * 2.获取可加载到ViewGroup中的View类型布局
 *
 * @author mnlin
 */
public interface SimulateDialogInterface<V extends View, L extends ViewGroup.LayoutParams> {
    /**
     * 获取布局
     *
     * <b>请确保: 多次调用该接口应当返回同一对象</b>
     *
     * @return View或者ViewGroup
     */
    @NonNull
    V generateView();

    /**
     * 获取layout-params,加载到FrameLayout中的方式
     */
    @Nullable
    L generateLayoutParams();
}

我们的容器通过该接口 中方法获取到view,然后在需要显示dialog时,将view 以指定的 layoutParams 形式添加到 自身容器,这样 dialog 就可以正常显示了。

除此之外,还提供 了自定义动画的接口ProvideIncludeDialogVGAnimator。通过该接口,可以自定义dialog弹出 和 关闭时候的动画;如果无从下手,可以参考默认提供的透明度变化动画:AlphaIDVGAnimatorImpl

注意:startAnimator与stopAnimator 方法的实现中,必须按照要求各自调用传入的最后一个runnable 对象,否则动画将无法显示

/**
 * Created on 2019/3/29  11:49
 * function : 为 {@link com.wallet.usdp.view.IncludeDialogViewGroup} 添加动画处理效果
 *
 * 注意:
 *      开始动画和结束动画前后,要保证 dialog的 状态完全恢复,否则,可能会影响弹出关闭后再次弹出的显示效果
 *
 * 注意:
 *      处理动画时,仅将 dialog.generateView 当做普通的{@link android.view.View} 来进行动画处理即可
 *
 * 例如:
 *
 * 当设置透明度变化动画时:
 *  在开始动画{@link ProvideIncludeDialogVGAnimator#startAnimator}前,要保证 view 透明度为最小值;以达到动画显示效果
 *  在结束动画 {@link ProvideIncludeDialogVGAnimator#stopAnimator}后,要保证 View 透明度为最大值,因为可能下次弹出此 dialog 时动画不再是透明度变化动画
 *
 * 详细逻辑可参考{@link com.wallet.usdp.util.AlphaIDVGAnimatorImpl}
 *
 * @author mnlin
 */
public interface ProvideIncludeDialogVGAnimator {
    /**
     * 开始动画 处理逻辑
     *
     * @param dialog                  被模拟的dialog
     * @param parent                  dialog-container
     * @param mustCallOnAnimatorStart 当自己动画开始之前,必须调用执行该 runnable  方法,保证dialog可以正常添加到屏幕上
     */
    void startAnimator(IncludeDialogViewGroup parent, SimulateDialogInterface<?, ?> dialog, Runnable mustCallOnAnimatorStart);

    /**
     * 结束动画 处理逻辑
     *
     * @param dialog                被模拟的dialog
     * @param parent                dialog-container
     * @param mustCallOnAnimatorEnd 当自己处理完动画后,必须调用执行该 runnable  方法,保证dialog可以正常关闭
     */
    void stopAnimator(IncludeDialogViewGroup parent, SimulateDialogInterface<?, ?> dialog, Runnable mustCallOnAnimatorEnd);
}

仅就功能来说,我们已经实现了统一的方式去弹出dialog,那么此种方法使用起来如何?我们来进行一下演示:

4、demo 使用

先给 出demo演示的效果:demo

在这里插入图片描述

使用方法很简单就 分为四步:

  1. 引入库,通过gradle形式,将库文件导入项目
  2. 修改默认的Activity继承关系,或者拷贝代码到基类中,实现返回键的处理逻辑
  3. 修改Activity根部局为 IncludeDialogViewGroup 容器,这个是dialog 可以弹出的前提
  4. 生成dialog 并进行注册到容器

这样所有的准备逻辑就完成了,为了不重复编写相同引导,这里将 GITHUB 地址给出:

https://github.com/lovingning/SimulateDialog

库中已详细说明了如何使用,这里就 不再赘述。

若有不解可直接下载 demo 查看显示 效果:

apk下载地址
demo地址

下一篇会依据前面 给出的系统已有的布局,探寻更简单的实现方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章