Android 导航栏虚拟按键 您所在的位置:网站首页 菜鸟商家寄件可以选快递吗 Android 导航栏虚拟按键

Android 导航栏虚拟按键

#Android 导航栏虚拟按键| 来源: 网络整理| 查看: 265

Android 虚拟按键

Android手机可分为有导航栏以及没导航栏两种,一般有物理按键的机器不会带有导航栏,而没有物理按键的机器则基本会带,比如华为的手机基本都是带导航栏的。当然现在全面屏手机大多都有全面屏手势,但这并不影响我们对导航栏的分析。

导航栏是如何加载到桌面上?是如何实现与物理按键相同的功能的呢?带着种种疑问,我们来read the fucking source code。

导航栏是属于系统界面的一部分,也就是SystemUI的一部分。在SystemUI中导航栏实质上是一个继承LinearLayout的ViewGroup:NavigationBarView,在系统界面初始化的时候在phone/StatusBar.java的makeStatusBarView方法中进行的

从状态栏入口函数makeStatusBarView开始看

protected void makeStatusBarView(@Nullable RegisterStatusBarResult result) { final Context context = mContext; updateDisplaySize(); // populates mDisplayMetrics updateResources(); updateTheme(); inflateStatusBarWindow(); //... createNavigationBar(result); }

进入 createNavigationBar 方法,发现主要是用NavigationBarController来管理

protected void createNavigationBar(@Nullable RegisterStatusBarResult result) { mNavigationBarController.createNavigationBars(true /* includeDefaultDisplay */, result); }

看NavigationBarController,调用的是NavigationBarFragment.create静态方法

public void createNavigationBars() { for (Display display : displays) { if (includeDefaultDisplay || display.getDisplayId() != DEFAULT_DISPLAY) { createNavigationBar(display, result); } } } void createNavigationBar(Display display, RegisterStatusBarResult result) { NavigationBarFragment.create(context, (tag, fragment) -> { NavigationBarFragment navBar = (NavigationBarFragment) fragment; }); }

看 NavigationBarFragment 的create方法,终于知道,先载入navigation_bar_window.xml创建一个navigationBarView对象,内部就是一个NavigationBarFrame extend FrameLayout,在这个View的onViewAttachedToWindow时用来加载NavigationBarFragment对象到其中,然后是WindowManager去addView了这个导航栏的navigationBarView,触发内部的NavigationBarFragment的onCreateView来加载实际的布局。(其实SystemUI所有的模块都是WindowManager来加载View)

public static View create(Context context, FragmentListener listener) { View navigationBarView = LayoutInflater.from(context).inflate( R.layout.navigation_bar_window, null); final NavigationBarFragment fragment = FragmentHostManager.get(navigationBarView) .create(NavigationBarFragment.class); navigationBarView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { final FragmentHostManager fragmentHost = FragmentHostManager.get(v); fragmentHost.getFragmentManager().beginTransaction() .replace(R.id.navigation_bar_frame, fragment, TAG) .commit(); fragmentHost.addTagListener(TAG, listener); } @Override public void onViewDetachedFromWindow(View v) { FragmentHostManager.removeAndDestroy(v); navigationBarView.removeOnAttachStateChangeListener(this); } }); context.getSystemService(WindowManager.class).addView(navigationBarView, lp); return navigationBarView; } navigation_bar_window.xml public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.navigation_bar, container, false); }

navigation_bar_window.xml只是多了一层嵌套,进入导航栏的真正根布局:navigation_bar.xml,好吧又是自定义view,又是嵌套结构,我们来看NavigationBarView和NavigationBarInflaterView

来看NavigationBarView的构造方法,就是初始化了一堆ButtonDispatcher对象,这个是用来处理虚拟按键图标显示的,之后再说

public NavigationBarView(Context context, AttributeSet attrs) { super(context, attrs); mButtonDispatchers.put(R.id.back, new ButtonDispatcher(R.id.back)); mButtonDispatchers.put(R.id.home, new ButtonDispatcher(R.id.home)); mButtonDispatchers.put(R.id.home_handle, new ButtonDispatcher(R.id.home_handle)); mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps)); //... }

而它实际生成NavigationBarInflaterView对象是在它的onFinishInflate方法中,NavigationBarView和NavigationBarInflaterView都是继承自View, 而onFinishInflate()方法,这是view的生命周期,每个view被inflate之后都会回调。两个类都Override了onFinishInflate方法,这个在inflate完会触发,其中NavigationBarInflaterView就在这个方法里调用inflateLayout用来创建虚拟按键布局

//NavigationBarView @Override public void onFinishInflate() { super.onFinishInflate(); mNavigationInflaterView = findViewById(R.id.navigation_inflater); mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers); } //NavigationBarInflaterView @Override protected void onFinishInflate() { super.onFinishInflate(); inflateChildren(); clearViews(); inflateLayout(getDefaultLayout()); }

我们大体看一下这个inflateLayout方法,通过getDefaultLayout获取导航栏的布局配置,然后进行解析

//NavigationInflaterView protected void inflateLayout(String newLayout) { mCurrentLayout = newLayout; if (newLayout == null) { newLayout = getDefaultLayout(); } //解析left[.5W],back[1WC];home;recent[1WC],right[.5W] String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3); if (sets.length != 3) { Log.d(TAG, "Invalid layout."); newLayout = getDefaultLayout(); sets = newLayout.split(GRAVITY_SEPARATOR, 3); } //GRAVITY_SEPARATOR=";" //BUTTON_SEPARATOR="," //split字符串 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, mHorizontal.findViewById(R.id.ends_group), false /* landscape */, true /* start */); inflateButtons(start, mVertical.findViewById(R.id.ends_group), true /* landscape */, true /* start */); inflateButtons(center, mHorizontal.findViewById(R.id.center_group), false /* landscape */, false /* start */); inflateButtons(center, mVertical.findViewById(R.id.center_group), true /* landscape */, false /* start */); inflateButtons(end, mHorizontal.findViewById(R.id.ends_group), false /* landscape */, false /* start */); inflateButtons(end, mVertical.findViewById(R.id.ends_group), true /* landscape */, false /* start */); updateButtonDispatchersCurrentView(); } //获取默认导航栏布局配置 protected String getDefaultLayout() { final int defaultResource = QuickStepContract.isGesturalMode(mNavBarMode) ? R.string.config_navBarLayoutHandle : mOverviewProxyService.shouldShowSwipeUpUI() ? R.string.config_navBarLayoutQuickstep : R.string.config_navBarLayout; return getContext().getString(defaultResource); }

下图中的三个绿框分别对应start、center、end 在这里插入图片描述

left[.5W],back[1WC];home;recent[1WC],right[.5W] back[1.7WC];home;contextual[1.7WC] back[40AC];home_handle;ime_switcher[40AC]

在看inflateButtons方法,调用createView创建相应View

private void inflateButtons() { for (int i = 0; i //创建相应布局 View v = createView(buttonSpec, parent, inflater); //调整大小 v = applySize(v, buttonSpec, landscape, start); parent.addView(v); //加到mButtonDispatchers中,设置图标 addToDispatchers(v); //... return v; } private View createView() { View v = null; //提炼字符串,去掉[],根据home,back,recent等对应不同的布局 String button = extractButton(buttonSpec); if (LEFT.equals(button)) { button = extractButton(NAVSPACE); } else if (RIGHT.equals(button)) { button = extractButton(MENU_IME_ROTATE); } 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); } else if (MENU_IME_ROTATE.equals(button)) { v = inflater.inflate(R.layounu_ime, parent, false); } else if (NAVSPACE.equals(button)) { v = inflater.inflate(R.layout.nav_key_space, parent, false); } else if (CLIPBOARD.equals(button)) { v = inflater.inflate(R.layout.clipboard, parent, false); //... return v; }

以HOME键为例, inflater.inflate(R.layout.home)就是解析的home.xml,返回KeyButtonView对象

//home.xml

applySize主要是控制所占布局的大小,[.5W]表示0.5个weight

private static final String WEIGHT_SUFFIX = "W"; private static final String WEIGHT_CENTERED_SUFFIX = "WC"; private static final String ABSOLUTE_SUFFIX = "A"; private static final String ABSOLUTE_VERTICAL_CENTERED_SUFFIX = "C"; private View applySize(View v, String buttonSpec, boolean landscape, boolean start) { String sizeStr = extractSize(buttonSpec); if (sizeStr == null) return v; if (sizeStr.contains(WEIGHT_SUFFIX) || sizeStr.contains(ABSOLUTE_SUFFIX)) { // To support gravity, wrap in RelativeLayout and apply gravity to it. // Children wanting to use gravity must be smaller then the frame. ReverseRelativeLayout frame = new ReverseRelativeLayout(mContext); LayoutParams childParams = new LayoutParams(v.getLayoutParams()); // Compute gravity to apply int gravity = (landscape) ? (start ? Gravity.TOP : Gravity.BOTTOM) : (start ? Gravity.START : Gravity.END); if (sizeStr.endsWith(WEIGHT_CENTERED_SUFFIX)) { gravity = Gravity.CENTER; } else if (sizeStr.endsWith(ABSOLUTE_VERTICAL_CENTERED_SUFFIX)) { gravity = Gravity.CENTER_VERTICAL; } // Set default gravity, flipped if needed in reversed layouts (270 RTL and 90 LTR) frame.setDefaultGravity(gravity); frame.setGravity(gravity); // Apply gravity to root frame.addView(v, childParams); if (sizeStr.contains(WEIGHT_SUFFIX)) { // Use weighting to set the width of the frame float weight = Float.parseFloat( sizeStr.substring(0, sizeStr.indexOf(WEIGHT_SUFFIX))); frame.setLayoutParams(new LinearLayout.LayoutParams(0, MATCH_PARENT, weight)); } else { int width = (int) convertDpToPx(mContext, Float.parseFloat(sizeStr.substring(0, sizeStr.indexOf(ABSOLUTE_SUFFIX)))); frame.setLayoutParams(new LinearLayout.LayoutParams(width, MATCH_PARENT)); } // Ensure ripples can be drawn outside bounds frame.setClipChildren(false); frame.setClipToPadding(false); return frame; } float size = Float.parseFloat(sizeStr); ViewGroup.LayoutParams params = v.getLayoutParams(); params.width = (int) (params.width * size); return v; }

布局和大小都已确定,那相应的图标是怎么设置的呢? 我们来看这个addToDispatchers(v)方法,v是createView生成的KeyButtonView

private void addToDispatchers(View v) { if (mButtonDispatchers != null) { final int indexOfKey = mButtonDispatchers.indexOfKey(v.getId()); if (indexOfKey >= 0) { mButtonDispatchers.valueAt(indexOfKey).addView(v); } if (v instanceof ViewGroup) { final ViewGroup viewGroup = (ViewGroup)v; final int N = viewGroup.getChildCount(); for (int i = 0; i mViews.add(view); if (view instanceof ButtonInterface) { final ButtonInterface button = (ButtonInterface) view; if (mImageDrawable != null) { button.setImageDrawable(mImageDrawable); } } }

在看这个mImageDrawable是哪来的,发现是ButtonDispatcher的setImageDrawable方法设置的,这个时候我们再回到NavigationBarView中搜索setImageDrawable方法,还是以HOME为例

public ButtonDispatcher getHomeButton() { return mButtonDispatchers.get(R.id.home); } public void updateNavButtonIcons() { KeyButtonDrawable homeIcon = mHomeDefaultIcon; getHomeButton().setImageDrawable(homeIcon); }

getHomeButton获得HOME对应的ButtonDispatcher对象,设置mHomeDefaultIcon。 对应图片资源为R.drawable.ic_sysbar_home,所以想要修改虚拟按键样式,直接替换相应资源即可

mHomeDefaultIcon = getHomeDrawable(); public KeyButtonDrawable getHomeDrawable() { final boolean quickStepEnabled = mOverviewProxyService.shouldShowSwipeUpUI(); KeyButtonDrawable drawable = quickStepEnabled ? getDrawable(R.drawable.ic_sysbar_home_quick_step) : getDrawable(R.drawable.ic_sysbar_home); orientHomeButton(drawable); return drawable; }

注意:为了方便多分辨适配,保证在高分辨率下也能显示精细画面,系统中大部分的图标资源都是矢量图,类似这样的,在替换的时候最好要求客户也提供这样的资源

ic_sysbar_home.xml

而实现实际HOME按键的功能逻辑其实很简单。 当布局都加载好,我们触摸BACK键的相应区域,触发对应KeyButtonView的onTouchEvent方法

//KeyButtonView public boolean onTouchEvent(MotionEvent ev) { final int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mDownTime = SystemClock.uptimeMillis(); if (mCode != KEYCODE_UNKNOWN) { sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime); } break; //... } return true; }

然后调用InputManager.injectInputEvent方法注入input事件,这里的mCode其实就是根据home.xml中的systemui:keyCode="3"得来的,对应KeyEvent.java中HOME键值的定义 public static final int KEYCODE_HOME = 3;

//KeyButtonView mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, KEYCODE_UNKNOWN); private void sendEvent(int action, int flags, long when) { final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, InputDevice.SOURCE_KEYBOARD); mInputManager.injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); }


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有