Android系统实现navigationbar订制

今天给大家带来一个有趣的实验,基于android N原生代码,实现动态显示和隐藏navigationbar的功能,先说下实现思路,

  • 在SettingsProvider中增加一个"show_navigation_bar"字段,用来存储当前是否显示和隐藏navigationbar的值
  • 在Settings中增加一个SwitchPreference,并且设置setOnPreferenceChangeListener,使用Settings.System.putString存储设置的值
  • 在SystemUI中注册一个ContentObserver,监听"show_navigation_bar"数据的变化,当发生变化时候,根据当前的值,决定是否显示或者隐藏navigationbar

那么最最重要的就是分析navigationbar的显示过程,从下面开始
##navigationbar的创建过程
在android中navigationbar是在SystemUI的PhoneStatusBar类加载显示的,如下:

PhoneStatusBar#makeStatusBarView

protected PhoneStatusBarView makeStatusBarView() {
       ......

        try {
            boolean showNav = mWindowManagerService.hasNavigationBar();
            if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav);
            if (showNav) {
                createNavigationBarView(context);
            }
        } catch (RemoteException ex) {
            // no window manager? good luck with that
        }
        ......

        return mStatusBarView;
}

这里根据读取到的配置文件决定是否显示navigationbar

mWindowManagerService.hasNavigationBar();

PhoneWindowManager#hasNavigationBar

@Override
    public boolean hasNavigationBar() {
        return mHasNavigationBar;
    }
mHasNavigationBar = res.getBoolean(com.android.internal.R.bool.config_showNavigationBar);

因此,我们可以修改frameworks/base/core/res/res/values文件中config.xml的"config_showNavigationBar"来决定是否显示navigationbar
当然,也可以通过SystemProperties的配置

##增加SystemProperties属性

在/system/sepolicy/property_contexts中添加
persist.shownav.   u:object_r:system_prop:s0
使用make -j4全编代码

这里写图片描述

然后在应用中设置和获取属性:

SystemProperties.set("persist.shownav.enable","false");
SystemProperties.getBoolean("persist.shownav.enable", true)

这样做虽然可以显示和隐藏navigationbar,但是有很大的缺陷,因为我们不能动态监听SystemProperties中的值发生变化,只能是每次生成的image文件,是打开或者关闭,因此如果想要让navigationbar的显示和隐藏由用户动态控制,是不可以的。

##动态显示和隐藏navigationbar
这里,其实我觉得如果要实现动态的改变和隐藏,可以添加一个类似于是否显示和隐藏的系统设置,下面实现动态显示和隐藏navigationbar,我们知道在android中,同样可以通过"Settings.System.putInt"或者"Settings.System.putString"来设置参数的存储,最终是存储到了SettingsProvider里

###在settings中添加设置项
这里我把设置选项添加到了DisplaySettings中

####添加字符串

packages/apps/Settings/res/values/strings.xml
"whether show nav"

####在布局中添加设置选项

packages/apps/Settings/res/xml/display_settings.xml

####在DisplaySettings中实现

public class DisplaySettings extends SettingsPreferenceFragment implements
        Preference.OnPreferenceChangeListener, Indexable {

    private SwitchPreference mshowNavPreference;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        ....
        mshowNavPreference = (SwitchPreference) findPreference("show_nav");
        if (mshowNavPreference != null) {
            // 显示之前的设置
            String isShownav = Settings.System.getString(getContentResolver(),"show_navigation_bar");
            mshowNavPreference.setChecked("true".equals(isShownav) ? true : false);
        }
        mshowNavPreference.setOnPreferenceChangeListener(this);
        ....
    }
 
    @Override
    public boolean onPreferenceChange(Preference preference, Object objValue) {
        ....
        if (preference == mshowNavPreference) {
            boolean auto = (Boolean) objValue;
            if (preference == mshowNavPreference) {
            boolean auto = (Boolean) objValue;
            Settings.System.putString(getContentResolver(),"show_navigation_bar",String.valueOf(auto));
        }
        }
        ....
    }
}

###在SettingsProvider中添加字段
数据库中数据的默认数据在frameworks/base/packages/SettingsProvider/res/values/defaults.xml中定义,因此我们需要在defaults.xml中添加默认值

true

在frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.Java中的loadSystemSettings()方法中加入新字段的添加代码

loadStringSetting(stmt, "show_navigation_bar",
                    R.string.def_show_nav_bar);

然后编译SettingsProvider完成之后,push apk到手机即可

在/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/show_navigation_bar.java中监听"show_navigation_bar"字段值的变化

public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
        DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener,
        HeadsUpManager.OnHeadsUpChangedListener {

    private void resetUserSetupObserver() {
        ....
        mContentObserver = new SettingsValueChangeContentObserver();
        mContext.getContentResolver().registerContentObserver(Settings.System.getUriFor("show_navigation_bar"),true, mContentObserver);
    }

    private SettingsValueChangeContentObserver mContentObserver;

    class SettingsValueChangeContentObserver extends ContentObserver {
 
        public SettingsValueChangeContentObserver() {
            super(new Handler());
        }
 
        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            Toast.makeText(mContext, Settings.System.getString(mContext.getContentResolver(),"show_navigation_bar"), Toast.LENGTH_SHORT).show();

            String isShownav = Settings.System.getString(mContext.getContentResolver(),"show_navigation_bar");
            if ("true".equals(isShownav)) {
                mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams());
            } else {
                mWindowManager.removeView(mNavigationBarView);
            }
        }  
    }
}

到此为止,就实现了根据用户的配置动态显示和隐藏navigationbar的功能,此时此刻,make -j8 全部编译代码,然后emulator启动生成的system.img文件,就可以看到效果了,如下:

Android系统实现navigationbar订制_第1张图片

由于下模拟器,效果可能会不是很好。
先看下效果吧:

Android系统实现navigationbar订制_第2张图片

##调整navigationbar的顺序
之前使用过HUAWEI的手机,发现其在设置中有一个功能,就是可以设置底部navigationbar的显示顺序,这个我认为对于用户体验来讲是有很大的进步,因为不同用户的使用喜欢习惯还是不同的,下面带大家实现这样的需求
###加载navigationbar布局
在正式开始之前,我们肯定是需要首先分析navigationbar布局,到现在,已经清楚NavigationBar的布局加载的
PhoneStatusBar#inflateNavigationBarView

protected void inflateNavigationBarView(Context context) {
        mNavigationBarView = (NavigationBarView) View.inflate(
                context, R.layout.navigation_bar, null);
}

navigation_bar.xml



    


可以看到这里实际上是显示的NavigationBarInflaterView,NavigationBarInflaterView继承自FrameLayout,那么对于布局的加载一定是在onFinishInflate中完成的
NavigationBarInflaterView#onFinishInflate

@Override
protected void onFinishInflate() {
        super.onFinishInflate();
        inflateChildren();
        clearViews();
        inflateLayout(getDefaultLayout());
}

NavigationBarInflaterView#inflateLayout

protected void inflateLayout(String newLayout) {
        mCurrentLayout = newLayout;
        if (newLayout == null) {
            newLayout = getDefaultLayout();
        }
        String[] 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);
        // Inflate these in start to end order or accessibility traversal will be messed up.
        inflateButtons(start, (ViewGroup) mRot0.findViewById(R.id.ends_group), false);
        inflateButtons(start, (ViewGroup) mRot90.findViewById(R.id.ends_group), true);
        ....
}

NavigationBarInflaterView#inflateButtons

private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape) {
        for (int i = 0; i < buttons.length; i++) {
            inflateButton(buttons[i], parent, landscape, i);
        }
}

NavigationBarInflaterView#inflateButtons

protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,
            int indexInParent) {
        LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
        float size = extractSize(buttonSpec);
        String button = extractButton(buttonSpec);
        View v = null;
        if (HOME.equals(button)) {
            v = inflater.inflate(R.layout.home, parent, false);
        } else if (BACK.equals(button)) {
            v = inflater.inflate(R.layout.back, parent, false);
        } else if (RECENT.equals(button)) {
            v = inflater.inflate(R.layout.recent_apps, parent, false);
        } 
        parent.addView(v);
        addToDispatchers(v);
        ...
        return v;
    }

可以看到上面那些代码最终添加的是"R.layout.home" , “R.layout.back” , “R.layout.recent_apps”, 而这些布局的添加是依据当前的button,这些button就是getDefaultLayout()中返回的,上面做了处理,将配置转换成字符串数组,那么我们实现navigationbar的按钮顺序,就可以从这个字符串入手了

这里的思路和之前一样:

  • 在SettingsProvider中增加一个字段, 用来存储当前设置显示何种布局
  • 在Settings中增加对应的ListPreference 供用户选择
  • 在PhoneStatusBar中增加ContentObserver监听设置的变化,当数据发生变化时候,重新加载"R.layout.navigation_bar" 布局,此时会执行NavigationBarInflaterView#getDefaultLayout,在getDefaultLayout中读取用户的设置,返回不同的布局配置

###SettingsProvider中增加字段
frameworks/base/packages/SettingsProvider/res/values/defaults.xml 中增加默认值

1

在DatabaseHelper#loadSystemSettings方法,加载该字段

loadIntegerSetting(stmt, "show_navigation_bar_method",R.integer.def_show_nav_bar_method);

###Settings中增加对应的ListPreference
首先需要定义设置项的用到的字符串和值,在packages/apps/Settings/res/values/arrays.xml文件中增加下面字符串数组


        back home recent
        recent back home
        home recent back
        back recent home
    

    
        1
        2
        3
        4
    

增加设置项,这里,同样的我在Display中显示该设置项。

在packages/apps/Settings/res/xml/display_settings.xml 中增加设置项


在DispalySettings中初始化,并设置点击事件

public class DisplaySettings extends SettingsPreferenceFragment implements
        Preference.OnPreferenceChangeListener, Indexable {

    private ListPreference mShowNavMethodPreference = null;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        mShowNavMethodPreference = (ListPreference)findPreference("show_nav_method");
        mShowNavMethodPreference.setOnPreferenceChangeListener(this);
    }

    @Override
    public boolean onPreferenceChange(Preference preference, Object objValue) {
         final String key = preference.getKey();
         if ("show_nav_method".equals(key)) {
            int value = Integer.parseInt((String) objValue);
            String summary = "";
            switch (value) {
		  case 1:
		      summary = getResources().getStringArray(R.array.show_nav_methods_entries)[0];
                      break;
                  case 2:
        	      summary = getResources().getStringArray(R.array.show_nav_methods_entries)[1];
                      break;
                  case 3:
        	      summary = getResources().getStringArray(R.array.show_nav_methods_entries)[2];
                      break;
                  case 4:
        	      summary = getResources().getStringArray(R.array.show_nav_methods_entries)[3];
                      break;
		  default:
	              summary = getResources().getStringArray(R.array.show_nav_methods_entries)[0];
                      break;
		}
            mShowNavMethodPreference.setSummary(summary);
            Settings.System.putInt(getContentResolver(), "show_navigation_bar_method", value);
	}
    }
}

###在SystemUI中添加navigationbar配置
这里的配置就是navigationbar的按钮顺序的配置。
在frameworks/base/packages/SystemUI/res/values/config.xml 文件下,增加下面代码

space,back;home;recent,menu_ime  
    recent,menu_ime;space,back;home   
    home;recent,menu_ime;space,back  
    space,back;recent,menu_ime;home   

###更改getDefaultLayout方法
需要更改NavigationBarInflaterView#getDefaultLayout, 目的是根据不同的设置提供不同的布局

protected String getDefaultLayout() {
        // 获取当前settings中设置值,决定显示那种布局
    	int showNavMethod = Settings.System.getInt(mContext.getContentResolver(),"show_navigation_bar_method",1);
    	switch (showNavMethod) {
	  case 1:
			  return mContext.getString(R.string.config_navBarLayout);
          case 2:
        	  return mContext.getString(R.string.config_navBarLayout_first);
          case 3:
        	  return mContext.getString(R.string.config_navBarLayout_second);
          case 4:
        	  return mContext.getString(R.string.config_navBarLayout_third);
		  default:
			  return mContext.getString(R.string.config_navBarLayout);
	}
}

###注册ContentObserver监听设置的变化
同样的,需要在PhoneStatusBar中增加ContentObserver监听设置的变化

private void resetUserSetupObserver() {
	....        
        mShowMethodObserver = new ShowMethodContentObserver();
        mContext.getContentResolver().registerContentObserver(Settings.System.getUriFor("show_navigation_bar_method"),true, mShowMethodObserver);
}

private ShowMethodContentObserver mShowMethodObserver;
    class ShowMethodContentObserver extends ContentObserver {
    	 
        public ShowMethodContentObserver() {
            super(new Handler());
        }
 
        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            Toast.makeText(mContext, "show method changed", Toast.LENGTH_SHORT).show();

            String isShownav = Settings.System.getString(mContext.getContentResolver(),"show_navigation_bar");
            if ("true".equals(isShownav)) {
                mNavigationBarView = (NavigationBarView) View.inflate(
                		mContext, R.layout.navigation_bar, null);
                if (null != mNavigationBarView && mNavigationBarView.getVisibility() == View.VISIBLE) {
                    mWindowManager.updateViewLayout(mNavigationBarView, getNavigationBarLayoutParams());
		} else {
		    mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams());
		}
                
            }  
        }
}

好了,到此为止,已经实现了一个简单的navigationbar的定制功能,效果如下:

Android系统实现navigationbar订制_第3张图片

Android系统实现navigationbar订制_第4张图片

Android系统实现navigationbar订制_第5张图片

ok,本篇博客剩下的内容,年后在处理,新年好呀新年好。

专注技术分享,包括Java,python,AI人工智能,Android分享,不定期更新学习视频,欢迎关注

你可能感兴趣的:(framework,android)