現在的App或多或少都會用到加載動畫,那麼如何將多樣性的加載動畫集成到我們的頁面框架中呢?
一些實現方式:
方式一:將加載動畫封裝成自定義View,在佈局中進行添加。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_project_select_designer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LoadingProgress
android:id="@+id/loading_progress"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
這樣的實現方式,需要在每一個使用加載動畫的佈局文件中添加LoadingProgress,並且在界面調用時控制顯示邏輯。如果大面積的使用到重複的代碼,我們可以對代碼進行封裝,例如封裝成一個RecyclerWithLoadAnimView,在該類中進行對LoadingProgress的顯示控制。
但是問題來了,如果有特定需求需要繼承RecyclerView實現特定功能呢?
所以我們還是不這樣寫了,爲了解耦合,我們還是講LoadingProgress放在別的地方去寫。
方式二:在BaseActivity中添加LoadingProgress
那麼問題來了,我們怎麼在不通過佈局文件,並且不影響子類的情況下,添加LoadingProgress呢?
其實很簡單,DecorView大家都知道吧,這是我們在onCreate() 的setContentView()方法返回的佈局的一個父容器。我們可以直接將LoadingProgress添加到佈局的一個父容器中就可以了,然後在BaseActivity中對LoadingProgress進行邏輯控制。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(...);
}
現在我們找一下這個DecorView。大家都知道,我們視圖都會放在Window裏,而Window類卻是一個抽象類,所以我們要找到它的子類,PhoneWindow類,就是這個類,我們的setContentView方法最終會調用PhoneWindow類中的
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
...
}
多餘的代碼我們不用看,我們只需要看installDecor()方法,這就是創建DecorView的方,我們看一下:
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
}
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
private final class DecorView extends FrameLayout{
...
}
這裏我們看到generateDecor()方法,這只是創建了一個DecorView,不過我們可以知道DecorView是FrameLayout的子類。
但是我們沒有找到對我們有利的東西,不過大家不要氣餒,相信有些人已經看到另一個對象mContentParent,顧名思義,它應該是內容的父類,我們繼續往下看 mContentParent = generateLayout(mDecor); 這個MContentParent是通過mDecor創建的。接下來,我們看一下generateLayout()方法:
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
protected ViewGroup generateLayout(DecorView decor) {
...
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
return contentParent;
}
通過對源碼的查看,我們知道了mContentParent是DecorView 的一個子View,而contentParent是通過findViewById()這個方法得到的,而這個Id就是 R.id.content ,所以我們找到了頁面的另一父容器mContentParent,並獲取到了它在佈局中的Id。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
這樣我們在BaseActivity中就能得到mContentParent對象了。而且它也是一個幀佈局,講到這裏應該比較清楚了,我們可以直接將LoadingProgress添加到mContentParent中,並在BaseActivity中進行邏輯控制。
public abstract class BaseActivity extends AppCompatActivity {
private LoadingProgress mProgress;
private List<View> views = new ArrayList<>();
private boolean isLoading = false;
private ViewGroup mContentView;
@Override
public void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
initView();
initData();
initListener();
}
@Override
public void setContentView(int layoutResID) {
super.setContentView(layoutResID);
//在setContentView() 中所設置佈局的父容器的ID 是 android.R.id.content
mContentView = (ViewGroup) findViewById(android.R.id.content);
setupLoadView();
}
/**
* 初始化加載動畫視圖
* 找到佈局中的所有一級子view
*/
private void setupLoadView() {
if (mProgress != null)
return;
mProgress = new LoadingProgress(this);
mProgress.setBackgroundResource(R.color.all_bg);
View contentView = mContentView.getChildAt(0);
if (contentView instanceof ViewGroup) {
ViewGroup contentGroup = (ViewGroup) contentView;
for (int i = 1; i < contentGroup.getChildCount(); i++) {
views.add(contentGroup.getChildAt(i));
}
}
int marginTop = getTitleHeight();
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
layoutParams.setMargins(0, marginTop, 0, 0);
mContentView.addView(mProgress, layoutParams);
}
/**
* 開啓動畫
*/
protected void openLoadAnim() {
if (isLoading || mProgress == null)
return;
mProgress.show();
isLoading = true;
}
/**
* 關閉動畫
*/
protected void closeLoadAnim() {
if (!isLoading || mProgress == null)
return;
mProgress.dismiss();
isLoading = false;
}
/**
* 初始化view
* 設置view 部分屬性(顯示隱藏等)
*/
protected abstract void initView();
/**
* 加載數據
* 在這裏調用 openLoadAnim() 方法
*/
protected abstract void initData();
/**
* 初始化監聽
*/
protected abstract void initListener();
/**
* 返回title高度,防止加載動畫格擋標題
*/
protected abstract int getTitleHeight();
/**
* 父容器獲取焦點,禁止子控件自動獲取焦點
* 佈局中有EditText時,禁止彈出軟鍵盤
*/
protected void containerFocus() {
mContentView.getChildAt(0).setFocusable(true);
}
@Override
protected void onDestroy() {
super.onDestroy();
closeLoadAnim();
mProgress = null;
views.clear();
views = null;
mContentView = null;
}
}
大家可能看到我沒有在onCreate中調用initView(),initData(),initListener()方法,而是在onPostCreate方法中調用。原因就留給大家去探索了。