關於作者
郭孝星,程式設計師,吉他手,主要從事Android平臺基礎架構方面的工作,歡迎交流技術方面的問題,可以去我的Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。
文章目錄
- 一 視窗型別
- 二 視窗引數
- 三 視窗模式
- 四 視窗回撥
- 五 視窗實現
從這篇文章開始,我們來分析和Window以及WindowManager相關的內容,
Abstract base class for a top-level window look and behavior policy.
Window在Android是一個視窗的概念,日常開發中我們和它接觸的不多,我們更多接觸的是View,但是View都是通過Window來呈現的,Window是View的直接管理者。
而WindowManager承擔者管理Window的責任。
一 視窗型別
Window在Android中有三種型別:
- 應用Window:z-index在1~99之間,它往往對應著一個Activity。
- 子Window:z-index在1000~1999之間,它往往不能獨立存在,需要依附在父Window上,例如Dialog等。
- 系統Window:z-index在2000~2999之間,它往往需要宣告許可權才能建立,例如Toast、狀態列、系統音量條、錯誤提示框都是系統Window。
z-index是Android視窗的層級的概念,z-index越大的視窗越居於頂層,
z-index對應著WindowManager.LayoutParams裡的type引數,具體說來。
應用Window
- public static final int FIRST_APPLICATION_WINDOW = 1;//1
- public static final int TYPE_BASE_APPLICATION = 1;//視窗的基礎值,其他的視窗值要大於這個值
- public static final int TYPE_APPLICATION = 2;//普通的應用程式視窗型別
- public static final int TYPE_APPLICATION_STARTING = 3;//應用程式啟動視窗型別,用於系統在應用程式視窗啟動前顯示的視窗。
- public static final int TYPE_DRAWN_APPLICATION = 4;
- public static final int LAST_APPLICATION_WINDOW = 99;//2
子Window
- public static final int FIRST_SUB_WINDOW = 1000;//子視窗型別初始值
- public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
- public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;
- public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2;
- public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3;
- public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW + 4;
- public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;
- public static final int LAST_SUB_WINDOW = 1999;//子視窗型別結束值
系統Window
- public static final int FIRST_SYSTEM_WINDOW = 2000;//系統視窗型別初始值
- public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;//系統狀態列視窗
- public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;//搜尋條視窗
- public static final int TYPE_PHONE = FIRST_SYSTEM_WINDOW+2;//通話視窗
- public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3;//系統ALERT視窗
- public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4;//鎖屏視窗
- public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;//TOAST視窗
二 視窗引數
在WindowManager裡定義了一個LayoutParams內部類,它描述了視窗的引數資訊,主要包括:
- public int x:視窗x軸座標
- public int y:視窗y軸座標
- public int type:視窗型別
- public int flags:視窗屬性
- public int softInputMode:輸入法鍵盤模式
關於視窗屬性,它控制著視窗的行為,舉幾個常見的:
- FLAG_ALLOW_LOCK_WHILE_SCREEN_ON 只要視窗可見,就允許在開啟狀態的螢幕上鎖屏
- FLAG_NOT_FOCUSABLE 視窗不能獲得輸入焦點,設定該標誌的同時,FLAG_NOT_TOUCH_MODAL也會被設定
- FLAG_NOT_TOUCHABLE 視窗不接收任何觸控事件
- FLAG_NOT_TOUCH_MODAL 在該視窗區域外的觸控事件傳遞給其他的Window,而自己只會處理視窗區域內的觸控事件
- FLAG_KEEP_SCREEN_ON 只要視窗可見,螢幕就會一直亮著
- FLAG_LAYOUT_NO_LIMITS 允許視窗超過螢幕之外
- FLAG_FULLSCREEN 隱藏所有的螢幕裝飾視窗,比如在遊戲、播放器中的全屏顯示
- FLAG_SHOW_WHEN_LOCKED 視窗可以在鎖屏的視窗之上顯示
- FLAG_IGNORE_CHEEK_PRESSES 當使用者的臉貼近螢幕時(比如打電話),不會去響應此事件
- FLAG_TURN_SCREEN_ON 視窗顯示時將螢幕點亮
關於視窗型別,它對應著視窗的層級,上面我們也提到過了。
它的建構函式也主要是針對這幾個引數的。
public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
public LayoutParams(int _type) {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = _type;
format = PixelFormat.OPAQUE;
}
public LayoutParams(int _type, int _flags) {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = _type;
flags = _flags;
format = PixelFormat.OPAQUE;
}
public LayoutParams(int _type, int _flags, int _format) {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = _type;
flags = _flags;
format = _format;
}
public LayoutParams(int w, int h, int _type, int _flags, int _format) {
super(w, h);
type = _type;
flags = _flags;
format = _format;
}
public LayoutParams(int w, int h, int xpos, int ypos, int _type,
int _flags, int _format) {
super(w, h);
x = xpos;
y = ypos;
type = _type;
flags = _flags;
format = _format;
}
}複製程式碼
三 視窗模式
關於視窗模式我們就比較熟悉了,我們會在AndroidManifest.xml裡Activity的標籤下設定android:windowSoftInputMode=”adjustNothing”,來控制輸入鍵盤顯示行為。
可選的有6個引數,原始碼裡也有6個值與之對應:
- SOFT_INPUT_STATE_UNSPECIFIED:沒有指定軟鍵盤輸入區域的顯示狀態。
- SOFT_INPUT_STATE_UNCHANGED:不要改變軟鍵盤輸入區域的顯示狀態。
- SOFT_INPUT_STATE_HIDDEN:在合適的時候隱藏軟鍵盤輸入區域,例如,當使用者導航到當前視窗時。
- SOFT_INPUT_STATE_ALWAYS_HIDDEN:當視窗獲得焦點時,總是隱藏軟鍵盤輸入區域。
- SOFT_INPUT_STATE_VISIBLE:在合適的時候顯示軟鍵盤輸入區域,例如,當使用者導航到當前視窗時。
- SOFT_INPUT_STATE_ALWAYS_VISIBLE:當視窗獲得焦點時,總是顯示軟鍵盤輸入區域。
當然,我們也可以通過程式碼設定鍵盤模式。
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);複製程式碼
四 視窗回撥
Window裡定義了一個Callback介面,Activity實現了Window.Callback介面,將Activity關聯給Window,Window就可以將一些事件交由Activity處理。
public interface Callback {
//鍵盤事件分發
public boolean dispatchKeyEvent(KeyEvent event);
//觸控事件分發
public boolean dispatchTouchEvent(MotionEvent event);
//軌跡球事件分發
public boolean dispatchTrackballEvent(MotionEvent event);
//可見性事件分發
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
//建立Panel View
public View onCreatePanelView(int featureId);
//建立menu
public boolean onCreatePanelMenu(int featureId, Menu menu);
//畫板準備好時回撥
public boolean onPreparePanel(int featureId, View view, Menu menu);
//menu開啟時回撥
public boolean onMenuOpened(int featureId, Menu menu);
//menu item被選擇時回撥
public boolean onMenuItemSelected(int featureId, MenuItem item);
//Window Attributes發生變化時回撥
public void onWindowAttributesChanged(WindowManager.LayoutParams attrs);
//Content View發生變化時回撥
public void onContentChanged();
//視窗焦點發生變化時回撥
public void onWindowFocusChanged(boolean hasFocus);
//Window被新增到WIndowManager時回撥
public void onAttachedToWindow();
//Window被從WIndowManager中移除時回撥
public void onDetachedFromWindow();
*/
//畫板關閉時回撥
public void onPanelClosed(int featureId, Menu menu);
//使用者開始執行搜尋操作時回撥
public boolean onSearchRequested();
}複製程式碼
五 視窗實現
Window是一個抽象類,它的唯一實現類是PhoneWindow,PhoneWindow裡包含了以下內容:
- private DecorView mDecor:DecorView是Activity中的頂級View,它本質上是一個FrameLayout,一般說來它內部包含標題欄和內容欄(com.android.internal.R.id.content)。
- ViewGroup mContentParent:視窗內容檢視,它是mDecor本身或者是它的子View。
- private ImageView mLeftIconView:左上角圖示
- private ImageView mRightIconView:右上角圖示
- private ProgressBar mCircularProgressBar:圓形loading條
- private ProgressBar mHorizontalProgressBar:水平loading條
- 其他的一些和轉場動畫相關的Transition與listener
看到這些,大家有沒有覺得很熟悉,這就是我們日常開發中經常見到的東西,它在PhoneWindow裡被建立。另外,我們在Activity裡經常呼叫的方法,它的實際實現也是
在PhoneWindow裡,我們分別來看一看。
setContentView()
這是一個我們非常熟悉的方法,只不過我們通常是在Activity裡進行呼叫,但是它的實際實現是在PhoneWindow裡。
public class PhoneWindow extends Window implements MenuBuilder.Callback {
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
//1. 如果沒有DecorView則建立它,並將建立好的DecorView賦值給mContentParent
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//2. 將Activity傳入的佈局檔案生成View並新增到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
//3. 回撥Window.Callback裡的onContentChanged()方法,這個Callback也被Activity
//所持有,因此它實際回撥的是Activity裡的onContentChanged()方法,通知Activity
//檢視已經發生改變。
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
}複製程式碼
這個方法主要做了兩件事情:
- 如果沒有DecorView則建立它,並將建立好的DecorView賦值給mContentParent
- 將Activity傳入的佈局檔案生成View並新增到mContentParent中
- 回撥Window.Callback裡的onContentChanged()方法,這個Callback也被Activity所持有,因此它實際回撥的是Activity裡的onContentChanged()方法,通知Activity檢視已經發生改變。
建立DecorView是通過installDecor()方法完成的,它的邏輯也非常簡單,就是建立了一個ViewGroup然後返回給了mDecor和mContentParent。
public class PhoneWindow extends Window implements MenuBuilder.Callback {
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//生成DecorView
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
} else {
...
}
...
}
}
protected ViewGroup generateLayout(DecorView decor) {
//讀取並設定主題顏色、狀態列顏色等資訊
...
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
//設定視窗引數等資訊
...
return contentParent;
}
}複製程式碼
通過以上這些流程,DecorView已經被建立並初始化完畢,Activity裡的佈局檔案也被成功的新增到PhoneWindow的mContentParent(實際上就是DecorView,它是Activity的頂層View)
中,但是這個時候DecorView還沒有真正的被WindowManager新增到Window中,它還無法接受使用者的輸入資訊和焦點事件,這個時候就相當於走到了Activity的onCreate()流程,介面還
未展示給使用者。
直到走到Activity的onResume()方法,它會呼叫Activity的makeVisiable()方法,DecorView才真正的被使用者所看到。
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback {
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
}複製程式碼
通常以上的分析,我們理解了setContentView的工作原理,另外還有addContentView、clearContentView,正如它們的名字那樣,setContentView是替換View,addContentView是新增View。實現原理相同。
好了,以上便是本篇文章的全部內容,下一篇文章我們來分析WindowManager的內容,分析Window的新增、移除和更新的流程。
附錄
文章末尾給大家提供一個WindowUtils工具類。
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.view.Surface;
import android.view.Window;
import android.view.WindowManager;
public final class WindowUtils {
/**
* Don`t let anyone instantiate this class.
*/
private WindowUtils() {
throw new Error("Do not need instantiate!");
}
/**
* 獲取當前視窗的旋轉角度
*
* @param activity activity
* @return int
*/
public static int getDisplayRotation(Activity activity) {
switch (activity.getWindowManager().getDefaultDisplay().getRotation()) {
case Surface.ROTATION_0:
return 0;
case Surface.ROTATION_90:
return 90;
case Surface.ROTATION_180:
return 180;
case Surface.ROTATION_270:
return 270;
default:
return 0;
}
}
/**
* 當前是否是橫屏
*
* @param context context
* @return boolean
*/
public static final boolean isLandscape(Context context) {
return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
}
/**
* 當前是否是豎屏
*
* @param context context
* @return boolean
*/
public static final boolean isPortrait(Context context) {
return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
}
/**
* 調整視窗的透明度 1.0f,0.5f 變暗
* @param from from>=0&&from<=1.0f
* @param to to>=0&&to<=1.0f
* @param context 當前的activity
*/
public static void dimBackground(final float from, final float to, Activity context) {
final Window window = context.getWindow();
ValueAnimator valueAnimator = ValueAnimator.ofFloat(from, to);
valueAnimator.setDuration(500);
valueAnimator.addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
WindowManager.LayoutParams params
= window.getAttributes();
params.alpha = (Float) animation.getAnimatedValue();
window.setAttributes(params);
}
});
valueAnimator.start();
}
}複製程式碼