從 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軸高度以及弧度。例如這樣:
<?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 在界面中進行顯示:
能否把該 佈局
include
到activity 的佈局
中,然後通過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
佈局了。
- 普通模式下(有 statusbar,有 toolbar):content 高度爲 1120 px,狀態欄48px,toolbar 112 px
- 簡潔模式下(有 statusbar,無 toolbar):content 高度爲 1232 px,其中狀態欄佔用了 48 px
- 全屏模式下(無 statusbar,無 toolbar):content 高度爲 1280 px,佔用全屏,windowTranslucentStatus模式與全屏模式相同
可見看到,幾種模式下,佈局的大小不盡相同,而我們要做的,是要針對所有的情況,都可以使用同一種方式去顯示dialog。
那麼我們使用 DecorView 是否可行?
這個問題留待下次再說,我們目前僅以一種簡單的方式來實現此種邏輯。
上面提到,要統一的進行彈窗,需要用到一個 FrameLayout 佈局,充當 dialog 容器,既然無法找到一個適配所有情況的容器,那麼不如干脆更簡單的一些,我們不再使用系統已有的佈局,現要求用戶必須在根部局處放置一個我們自定義的 FrameLayout 佈局,該佈局中我們修改了部分邏輯,然後定義一些通用的方法:
那麼現在來大概猜想一下,此FrameLayout佈局大致需要包含的功能:
- 可以顯示 dialog,可以顯示多個dialog
- 可以隱藏 dialog,可以隱藏多個dialog
- 需要爲新創建的 dialog 提供註冊接口 ,否則此 dialog容器無法獲知有哪些 dialog 存在
- 提供註銷接口,爲不再需要彈出顯示 dialog 保留移除功能
- 判斷當前是否有 dialog 在顯示,爲主activity中必要的邏輯做準備
- 最後,爲用戶保留自定義動畫的接口,以實現動態美觀的效果
看起來可能功能比較 麻煩,但其實都 很簡單,邏輯很清晰,按照每個邏輯一個方法來設計,大概我們做如下功能: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
}
}
其實這段代碼,除了實現上述基本 功能外,還多了一些邏輯,包括:
- 提供 shadeView 遮罩,在模擬的 dialog 彈出時,可以模擬一層覆蓋物,使顯示效果更接近 原生 dialog。
- 在 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
使用方法很簡單就 分爲四步:
- 引入庫,通過gradle形式,將庫文件導入項目
- 修改默認的Activity繼承關係,或者拷貝代碼到基類中,實現返回鍵的處理邏輯
- 修改Activity根部局爲
IncludeDialogViewGroup
容器,這個是dialog 可以彈出的前提 - 生成dialog 並進行註冊到容器
這樣所有的準備邏輯就完成了,爲了不重複編寫相同引導,這裏將 GITHUB 地址給出:
https://github.com/lovingning/SimulateDialog
庫中已詳細說明了如何使用,這裏就 不再贅述。
若有不解可直接下載 demo 查看顯示 效果:
下一篇會依據前面 給出的系統已有的佈局,探尋更簡單的實現方式。