android fragment重新載入_NavigationBar載入與按鍵處理
技術標籤:android fragment重新載入
NavigationBar載入流程
NavigationBar的載入時機是放在Statusbar載入流程中的makeStatusBarView中,在createNavigationBar中進行建立的,程式碼如下:
protectedvoidcreateNavigationBar(){
//執行create進行初始化
mNavigationBarView=NavigationBarFragment.create(mContext,(tag,fragment)->{
mNavigationBar=(NavigationBarFragment)fragment;
//傳入LightBarController,實現亮暗主題
if(mLightBarController!=null){
mNavigationBar.setLightBarController(mLightBarController);
}
//更新當前SystemUiVisibility
mNavigationBar.setCurrentSysuiVisibility(mSystemUiVisibility);
});
}
從上面可以看出,NavigationBar其實是一個Fragment,顧其遵循Fragment的生命週期。在create函式中,初始化導航欄window屬性,並將其通過windowManager新增到系統視窗,並通過FragmentManager事物啟動NavigationBarFragment。具體邏輯如下:
publicstaticViewcreate(Contextcontext,FragmentListenerlistener){
WindowManager.LayoutParamslp=newWindowManager.LayoutParams(
LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
|WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH//監聽outside事件,供Deadzone使用
|WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
|WindowManager.LayoutParams.FLAG_SLIPPERY,
PixelFormat.TRANSLUCENT);
lp.token=newBinder();
lp.setTitle("NavigationBar");
lp.accessibilityTitle=context.getString(R.string.nav_bar);
lp.windowAnimations=0;
ViewnavigationBarView=LayoutInflater.from(context).inflate(
R.layout.navigation_bar_window,null);
if(DEBUG)Log.v(TAG,"addNavigationBar:abouttoadd"+navigationBarView);
if(navigationBarView==null)returnnull;
context.getSystemService(WindowManager.class).addView(navigationBarView,lp);
FragmentHostManagerfragmentHost=FragmentHostManager.get(navigationBarView);
NavigationBarFragmentfragment=newNavigationBarFragment();
fragmentHost.getFragmentManager().beginTransaction()
.replace(R.id.navigation_bar_frame,fragment,TAG)
.commit();
fragmentHost.addTagListener(TAG,listener);
returnnavigationBarView;
}
這裡可以看到建立NavigationBarView的時候,其LayoutParams中有一個flag:WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,該flag的作用是監聽outside的touch事件,為實現導航欄防誤觸功能,詳細可以參考此文章:SystemUI之DeadZone分析。最後inflate了一個FrameLayout佈局"R.layout.navigation_bar_window",然後通過FragmentManager把NavigationBarFragment物件給替換進去,觸發Fragment生命週期裡的onCreateView方法。方法實現如下:
@Override
publicViewonCreateView(LayoutInflaterinflater,@NullableViewGroupcontainer,
BundlesavedInstanceState){
returninflater.inflate(R.layout.navigation_bar,container,false);
}
這裡關注一下navigation_bar.xml的實現:
<com.android.systemui.statusbar.phone.NavigationBarViewxmlns:android="http://schemas.android.com/apk/res/android"xmlns:systemui="http://schemas.android.com/apk/res-auto"android:layout_height="match_parent"android:layout_width="match_parent"android:background="@drawable/system_bar_background">
<com.android.systemui.statusbar.phone.NavigationBarInflaterViewandroid:id="@+id/navigation_inflater"android:layout_width="match_parent"android:layout_height="match_parent"/>
com.android.systemui.statusbar.phone.NavigationBarView>
可以看到其實最終按鍵的父容器是NavigationBarInflaterView,這裡我們詳細看下這個自定義view的載入過程,首先是它的構造方法:
publicNavigationBarInflaterView(Contextcontext,AttributeSetattrs){
super(context,attrs);
//建立橫豎屏對應的Inflater
createInflaters();
mDisplay=((WindowManager)
context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
ModedisplayMode=mDisplay.getMode();
isRot0Landscape=displayMode.getPhysicalWidth()>displayMode.getPhysicalHeight();
//初始化overviewService
mOverviewProxyService=Dependency.get(OverviewProxyService.class);
}
在構造方法中的createInflaters方法,該方法的作用就是為了建立橫豎屏對應的Inflater。在onFinishInflate進行NavigationBarInflaterView初始化:
@Override
protectedvoidonFinishInflate(){
super.onFinishInflate();
//將橫豎屏的viewgroup加入到父容器
inflateChildren();
clearViews();
//載入佈局
inflateLayout(getDefaultLayout());
}
這裡主要進行了橫豎屏NavigationBar按鈕父容器的建立,並通過inflateLayout方法載入對應的佈局。這裡重點關注一下inflateLayout和getDefaultLayout方法:
protectedvoidinflateLayout(StringnewLayout){
mCurrentLayout=newLayout;
if(newLayout==null){
newLayout=getDefaultLayout();
}
String[]sets=newLayout.split(GRAVITY_SEPARATOR,3);
if(sets.length!=3){
Log.d(TAG,"Invalidlayout.");
newLayout=getDefaultLayout();
sets=newLayout.split(GRAVITY_SEPARATOR,3);
}
String[]start=sets[0].split(BUTTON_SEPARATOR);
String[]center=sets[1].split(BUTTON_SEPARATOR);
String[]end=sets[2].split(BUTTON_SEPARATOR);
//Inflatetheseinstarttoendorderoraccessibilitytraversalwillbemessedup.
inflateButtons(start,mRot0.findViewById(R.id.ends_group),isRot0Landscape,true);
inflateButtons(start,mRot90.findViewById(R.id.ends_group),!isRot0Landscape,true);
inflateButtons(center,mRot0.findViewById(R.id.center_group),isRot0Landscape,false);
inflateButtons(center,mRot90.findViewById(R.id.center_group),!isRot0Landscape,false);
addGravitySpacer(mRot0.findViewById(R.id.ends_group));
addGravitySpacer(mRot90.findViewById(R.id.ends_group));
inflateButtons(end,mRot0.findViewById(R.id.ends_group),isRot0Landscape,false);
inflateButtons(end,mRot90.findViewById(R.id.ends_group),!isRot0Landscape,false);
updateButtonDispatchersCurrentView();
}
protectedStringgetDefaultLayout(){
finalintdefaultResource=mOverviewProxyService.shouldShowSwipeUpUI()
?R.string.config_navBarLayoutQuickstep
:R.string.config_navBarLayout;
returnmContext.getString(defaultResource);
}
這裡通過getDefaultLayout獲取當前導航欄模式的佈局字串,在inflateLayout中解析,並通過inflateButtons加到橫豎屏對應的父容器中,這裡進一步看一下inflateButtons方法:
@Nullable
protectedViewinflateButton(StringbuttonSpec,ViewGroupparent,booleanlandscape,booleanstart){
LayoutInflaterinflater=landscape?mLandscapeInflater:mLayoutInflater;
//建立對應的iconview,供後面新增到父容器中
Viewv=createView(buttonSpec,parent,inflater);
if(v==null)returnnull;
v=applySize(v,buttonSpec,landscape,start);
parent.addView(v);
addToDispatchers(v);
ViewlastView=landscape?mLastLandscape:mLastPortrait;
ViewaccessibilityView=v;
if(vinstanceofReverseRelativeLayout){
accessibilityView=((ReverseRelativeLayout)v).getChildAt(0);
}
if(lastView!=null){
accessibilityView.setAccessibilityTraversalAfter(lastView.getId());
}
if(landscape){
mLastLandscape=accessibilityView;
}else{
mLastPortrait=accessibilityView;
}
returnv;
}
這裡其實就是根據一些列傳入的引數,構建對應的view,然後新增到父容器中,實現邏輯是createView方法,其程式碼邏輯如下:
privateViewcreateView(StringbuttonSpec,ViewGroupparent,LayoutInflaterinflater){
Viewv=null;
Stringbutton=extractButton(buttonSpec);
if(LEFT.equals(button)){
Strings=Dependency.get(TunerService.class).getValue(NAV_BAR_LEFT,NAVSPACE);
button=extractButton(s);
}elseif(RIGHT.equals(button)){
Strings=Dependency.get(TunerService.class).getValue(NAV_BAR_RIGHT,MENU_IME_ROTATE);
button=extractButton(s);
}
//Letpluginsgofirstsotheycanoverrideastandardviewiftheywant.
for(NavBarButtonProviderprovider:mPlugins){
v=provider.createView(buttonSpec,parent);
if(v!=null)returnv;
}
if(HOME.equals(button)){
v=inflater.inflate(R.layout.home,parent,false);
}elseif(BACK.equals(button)){
v=inflater.inflate(R.layout.back,parent,false);
}elseif(RECENT.equals(button)){
v=inflater.inflate(R.layout.recent_apps,parent,false);
}elseif(MENU_IME_ROTATE.equals(button)){
v=inflater.inflate(R.layout.menu_ime,parent,false);
}elseif(NAVSPACE.equals(button)){
v=inflater.inflate(R.layout.nav_key_space,parent,false);
}elseif(CLIPBOARD.equals(button)){
v=inflater.inflate(R.layout.clipboard,parent,false);
}elseif(CONTEXTUAL.equals(button)){
v=inflater.inflate(R.layout.contextual,parent,false);
}elseif(button.startsWith(KEY)){
Stringuri=extractImage(button);
intcode=extractKeycode(button);
v=inflater.inflate(R.layout.custom_key,parent,false);
((KeyButtonView)v).setCode(code);
if(uri!=null){
if(uri.contains(":")){
((KeyButtonView)v).loadAsync(Icon.createWithContentUri(uri));
}elseif(uri.contains("/")){
intindex=uri.indexOf('/');
Stringpkg=uri.substring(0,index);
intid=Integer.parseInt(uri.substring(index+1));
((KeyButtonView)v).loadAsync(Icon.createWithResource(pkg,id));
}
}
}
returnv;
}
這裡的邏輯就是根據傳入的引數,create不一樣的BottonView,然後通過inflateButton中的applysize方法設定其對應的Layout屬性,最後新增到對應的父佈局中。
而在NavigationBarInflaterView的父佈局NavigationbarView中,對導航欄上所有icon,進行了image res的更新,實現方法是updateIcons,具體邏輯如下:
privatevoidupdateIcons(Contextctx,ConfigurationoldConfig,ConfigurationnewConfig){
intdualToneDarkTheme=Utils.getThemeAttr(ctx,R.attr.darkIconTheme);
intdualToneLightTheme=Utils.getThemeAttr(ctx,R.attr.lightIconTheme);
ContextlightContext=newContextThemeWrapper(ctx,dualToneLightTheme);
ContextdarkContext=newContextThemeWrapper(ctx,dualToneDarkTheme);
if(oldConfig.orientation!=newConfig.orientation
||oldConfig.densityDpi!=newConfig.densityDpi){
mDockedIcon=getDrawable(lightContext,darkContext,R.drawable.ic_sysbar_docked);
mHomeDefaultIcon=getHomeDrawable(lightContext,darkContext);
}
if(oldConfig.densityDpi!=newConfig.densityDpi
||oldConfig.getLayoutDirection()!=newConfig.getLayoutDirection()){
mBackIcon=getBackDrawable(lightContext,darkContext);
mRecentIcon=getDrawable(lightContext,darkContext,R.drawable.ic_sysbar_recent);
mMenuIcon=getDrawable(lightContext,darkContext,R.drawable.ic_sysbar_menu);
mAccessibilityIcon=getDrawable(lightContext,darkContext,
R.drawable.ic_sysbar_accessibility_button,false/*hasShadow*/);
mImeIcon=getDrawable(lightContext,darkContext,R.drawable.ic_ime_switcher_default,
false/*hasShadow*/);
updateRotateSuggestionButtonStyle(mRotateBtnStyle,false);
if(ALTERNATE_CAR_MODE_UI){
updateCarModeIcons(ctx);
}
}
}
這裡根據lightContext和darkContext,使我們導航欄的按鈕擁有兩套對應的圖示,以此來適配亮暗主題風格的導航欄。
按鍵處理流程
導航欄中每個button都有自己的佈局檔案,且除了Recents鍵,其他按鈕都有對應的keycode,用於產生KEY_BACK、KEY_HOME以及KEY_MENU等按鍵事件。而recents鍵實現了點選事件,直接啟動最近任務介面。
這裡以home鍵為例,梳理一下虛擬按鍵的流程,首先先看home.xml的佈局檔案:
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:id="@+id/home"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:layout_weight="0"
systemui:keyCode="3"
android:scaleType="center"
android:contentDescription="@string/accessibility_home"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"
/>
可以看到,其實home鍵就是一個KeyButtonView的自定義view,在佈局檔案中設定了其對應的keyCode。那如何將keyCode傳給系統的呢?這裡需要看一下KeyButtonView的onTouchEvent方法:
publicbooleanonTouchEvent(MotionEventev){
finalbooleanshowSwipeUI=mOverviewProxyService.shouldShowSwipeUpUI();
finalintaction=ev.getAction();
intx,y;
if(action==MotionEvent.ACTION_DOWN){
mGestureAborted=false;
}
if(mGestureAborted){
setPressed(false);
returnfalse;
}
switch(action){
caseMotionEvent.ACTION_DOWN:
mDownTime=SystemClock.uptimeMillis();
mLongClicked=false;
setPressed(true);
//UserawXandYtodetectgesturesincaseaparentchangesthexandyvalues
mTouchDownX=(int)ev.getRawX();
mTouchDownY=(int)ev.getRawY();
//如果keycode不是0,就給InputDispatcher建立傳送一個Actiondown事件
if(mCode!=0){
sendEvent(KeyEvent.ACTION_DOWN,0,mDownTime);
}else{
//Providethesamehapticfeedbackthatthesystemoffersforvirtualkeys.
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
}
if(!showSwipeUI){
playSoundEffect(SoundEffectConstants.CLICK);
}
removeCallbacks(mCheckLongPress);
postDelayed(mCheckLongPress,ViewConfiguration.getLongPressTimeout());
break;
caseMotionEvent.ACTION_MOVE:
x=(int)ev.getRawX();
y=(int)ev.getRawY();
booleanexceededTouchSlopX=Math.abs(x-mTouchDownX)>(mIsVertical
?NavigationBarCompat.getQuickScrubTouchSlopPx()
:NavigationBarCompat.getQuickStepTouchSlopPx());
booleanexceededTouchSlopY=Math.abs(y-mTouchDownY)>(mIsVertical
?NavigationBarCompat.getQuickStepTouchSlopPx()
:NavigationBarCompat.getQuickScrubTouchSlopPx());
if(exceededTouchSlopX||exceededTouchSlopY){
//Whenquickstepisenabled,preventanimatingtherippletriggeredby
//setPressedanddecidetorunitontouchup
setPressed(false);
removeCallbacks(mCheckLongPress);
}
break;
caseMotionEvent.ACTION_CANCEL:
setPressed(false);
if(mCode!=0){
//當觸控事件被取消,傳送一個KeyEvent.FLAG_CANCELED的UP事件
sendEvent(KeyEvent.ACTION_UP,KeyEvent.FLAG_CANCELED);
}
removeCallbacks(mCheckLongPress);
break;
caseMotionEvent.ACTION_UP:
finalbooleandoIt=isPressed()&&!mLongClicked;
setPressed(false);
finalbooleandoHapticFeedback=(SystemClock.uptimeMillis()-mDownTime)>150;
if(showSwipeUI){
if(doIt){
//Applyhapticfeedbackontouchupsincethereisnoneontouchdown
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
playSoundEffect(SoundEffectConstants.CLICK);
}
}elseif(doHapticFeedback&&!mLongClicked){
//Alwayssendareleaseourselvesbecauseitdoesn'tseemtobesentelsewhere
//anditfeelsweirdtosometimesgetareleasehapticandothertimesnot.
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
}
if(mCode!=0){
if(doIt){
//傳送UP事件
sendEvent(KeyEvent.ACTION_UP,0);
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
}else{
sendEvent(KeyEvent.ACTION_UP,KeyEvent.FLAG_CANCELED);
}
}else{
//nokeycode,justaregularImageView
//如果沒有設定keycode,則去執行onclick事件
if(doIt&&mOnClickListener!=null){
mOnClickListener.onClick(this);
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
}
}
removeCallbacks(mCheckLongPress);
break;
}
returntrue;
}
這裡其實就和普通的自定義view一樣,對觸控事件進行依次處理。然後通過sendEvent方法派發對應的touch event事件,如果沒有設定keycode(如recent鍵),就執行onclick事件。在sendEvent方法中通過InputManager下發對應事件,邏輯如下:
voidsendEvent(intaction,intflags,longwhen){
mMetricsLogger.write(newLogMaker(MetricsEvent.ACTION_NAV_BUTTON_EVENT)
.setType(MetricsEvent.TYPE_ACTION)
.setSubtype(mCode)
.addTaggedData(MetricsEvent.FIELD_NAV_ACTION,action)
.addTaggedData(MetricsEvent.FIELD_FLAGS,flags));
finalintrepeatCount=(flags&KeyEvent.FLAG_LONG_PRESS)!=0?1:0;
finalKeyEventev=newKeyEvent(mDownTime,when,action,mCode,repeatCount,
0,KeyCharacterMap.VIRTUAL_KEYBOARD,0,
flags|KeyEvent.FLAG_FROM_SYSTEM|KeyEvent.FLAG_VIRTUAL_HARD_KEY,
InputDevice.SOURCE_KEYBOARD);
InputManager.getInstance().injectInputEvent(ev,
InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
最終keycode處理都是到PhoneWindowManager中處理的,感興趣的同學,可以進一步閱讀原始碼分析。
— — — END — — —
運營人員:Monster