在Activity或Fragment頁面動態添加View,有其應用場景,好比配合運營在首頁動態插入H5活動頁(以下圖手淘的雪花例示[1]
),在頁面頭部插入通知View等。本文結合ActivityLifecycleCallbacks[2]
及DecorView使用,爲相似需求提供一種解決方案。html
本文方案監聽Activity生命週期,在拿到指定Activity後,獲取PhoneWindow.mContentParent,並在mContentParent中addView。android
在android API 14+ ,Application.registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback)提供了一套回調方法,用於對Activity的生命週期事件進行集中處理。咱們可用ActivityLifecycleCallbacks玩出不少花樣,好比頁面埋點,router預處理,限制Activity實例個數,swipeback等等。在本文它keep當前activity引用。git
public interface ActivityLifecycleCallbacks { void onActivityCreated(Activity activity, Bundle savedInstanceState); void onActivityStarted(Activity activity); void onActivityResumed(Activity activity); void onActivityPaused(Activity activity); void onActivityStopped(Activity activity); void onActivitySaveInstanceState(Activity activity, Bundle outState); void onActivityDestroyed(Activity activity); }
Android的頁面組成[3]
以下圖(注:在sdk 14+或在19+AppCompat,mContentParent不包含Actionbar,既標題欄[4]
),咱們關心如何獲取mContentParent。github
在此簡單例出mContentParent的相關代碼app
/** *Activity.setContentView,此處的getWindow返回PhoneWindow,在 *activity.attach方法中實例化 **/ public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); } /** *PhoneWindow.setContentView **/ public void setContentView(int layoutResID) { ... if (mContentParent == null) { mContentParent = generateLayout(mDecor); } ... } /** * mContentParent 從DecorView中ID爲ID_ANDROID_CONTENT中賦值;ID_ANDROID_CONTENT在PhoneWindow中是一個int常量 * public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content; **/ protected ViewGroup generateLayout(DecorView decor) { ... ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); if (contentParent == null) { throw new RuntimeException("Window couldn't find content container view"); } ... return contentParent; } /** *Activity.findViewById **/ public View findViewById(@IdRes int id) { return getWindow().findViewById(id); } /** *PhoneWindow.findViewById **/ public View findViewById(@IdRes int id) { return getDecorView().findViewById(id); }
從上述代碼咱們能夠發現,mContentParent能夠經過activity.findViewById(android.R.id.content)獲取。ide
此方案兩個關鍵:keep Activity實例和獲取id爲android.R.id.content的View。下面列出核心代碼。post
public class Background implements Application.ActivityLifecycleCallbacks { /** * 用於檢測當前APP是否運行於前臺 */ private int appCount = 0; private WeakReference<Activity> mActivity; @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { } @Override public void onActivityStarted(Activity activity) { appCount++; } @Override public void onActivityResumed(Activity activity) { mActivity = new WeakReference<>(activity); } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { appCount--; } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { } /** * 判斷當前APP是否在後臺 * * @param context * @return */ public boolean inBackRunning(final Context context) { PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); boolean isScreenOn = true; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { isScreenOn = pm.isInteractive(); } else { isScreenOn = pm.isScreenOn(); } boolean isOnForeground = appCount > 0; return !isScreenOn || !isOnForeground; } /** * 獲取當前頁面的Fragment * * @return */ public Fragment getCurFragment() { if (mActivity == null ||mActivity.get()==null|| !(mActivity.get() instanceof FragmentActivity)) { return null; } FragmentManager fragManager = ((FragmentActivity) mActivity.get()).getSupportFragmentManager(); if (fragManager.getFragments() != null) { List<Fragment> fragments = fragManager.getFragments(); for (Fragment fragment : fragments) { if (fragment != null && fragment.isVisible()) return fragment; } return null; } return null; } /** * 獲取當前運行的Activity * * @return */ public Activity getCurActivity() { return mActivity.get(); } } /** * 頁面頂部顯示通知View實例 `[5]` **/ public class MessageBar extends FrameLayout { private Runnable mDismissRunnable = new Runnable() { @Override public void run() { dismiss(); } }; private AppMessage mAppMessage = new AppMessage.Builder().build(); public MessageBar(Context context) { super(context); } public MessageBar(Context context, AttributeSet attrs) { super(context, attrs); } public MessageBar(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public static MessageBar with(Context context) { return new MessageBar(context); } public MessageBar setAppMessage(AppMessage appMessage) { if (appMessage != null) { mAppMessage = appMessage; } return this; } public void dismiss() { finish(); } private void finish() { clearAnimation(); ViewGroup parent = (ViewGroup) getParent(); if (parent != null) { parent.removeView(this); } } /** * Displays the {@link MessageBar} at the bottom of the * {@link Activity} provided. * * @param targetActivity */ public void show(Activity targetActivity) { ViewGroup root = (ViewGroup) targetActivity.findViewById(android.R.id.content); MarginLayoutParams params = init(targetActivity, root); showInternal(targetActivity, params, root); MediaUtil.getInstance(targetActivity).playSound(R.raw.im_notification, targetActivity); VibratorUtil.vibrator(targetActivity); } private MarginLayoutParams init(Activity targetActivity, ViewGroup parent) { FrameLayout layout = (FrameLayout) LayoutInflater.from(targetActivity) .inflate(R.layout.mercury_template, this, true); customUI(layout); MarginLayoutParams params; params = createMarginLayoutParams( parent, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); return params; } private void customUI(FrameLayout layout) { //TODO init custom view } private static MarginLayoutParams createMarginLayoutParams(ViewGroup viewGroup, int width, int height) { if (viewGroup instanceof FrameLayout) { LayoutParams params = new LayoutParams(width, height); params.gravity = TOP; return params; } else if (viewGroup instanceof RelativeLayout) { RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(width, height); params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE); return params; } else if (viewGroup instanceof LinearLayout) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(width, height); params.gravity = TOP; return params; } else { throw new IllegalStateException("Requires FrameLayout or RelativeLayout for the parent of Snackbar"); } } private void showInternal(Activity targetActivity, MarginLayoutParams params, ViewGroup parent) { parent.removeView(this); // We need to make sure the MessageBar elevation is at least as high as // any other child views, or it will be displayed underneath them if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { for (int i = 0; i < parent.getChildCount(); i++) { View otherChild = parent.getChildAt(i); float elvation = otherChild.getElevation(); if (elvation > getElevation()) { setElevation(elvation); } } } parent.addView(this, params); bringToFront(); // As requested in the documentation for bringToFront() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { parent.requestLayout(); parent.invalidate(); } focusForAccessibility(this); startTimer(); } private void startTimer() { postDelayed(mDismissRunnable, 3000); } private void focusForAccessibility(View view) { final AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); AccessibilityEventCompat.asRecord(event).setSource(view); try { view.sendAccessibilityEventUnchecked(event); } catch (IllegalStateException e) { // accessibility is off. } } }
[1] 利用動態加載實現手機淘寶的節日特效:http://www.jianshu.com/p/195e...
[2] ActivityLifecycleCallbacks: https://developer.android.com...
[3] setContentView 背後那些事兒:http://www.jianshu.com/p/0219...
[4] android.R.id.content指什麼以及一個實例:http://lingnanlu.github.io/20...
[5] snackbar:https://github.com/nispok/sna...ui