当前位置: 移动技术网 > IT编程>移动开发>Android > Android 8.1 SystemUI虚拟导航键加载流程解析

Android 8.1 SystemUI虚拟导航键加载流程解析

2019年05月15日  | 移动技术网IT编程  | 我要评论

东营新世纪人才市场,乌龙小皇拳,生猪价格 今日猪价

需求

基于mtk 8.1平台定制导航栏部分,在左边增加音量减,右边增加音量加

思路

需求开始做之前,一定要研读systemui navigation模块的代码流程!!!不要直接去网上copy别人改的需求代码,盲改的话很容易出现问题,然而无从解决。网上有老平台(8.0-)的讲解system ui的导航栏模块的博客,自行搜索。8.0对system ui还是做了不少细节上的改动,代码改动体现上也比较多,但是总体基本流程并没变。

源码阅读可以沿着一条线索去跟代码,不要过分在乎代码细节!例如我客制化这个需求,可以跟着导航栏的返回(back),桌面(home),最近任务(recent)中的一个功能跟代码流程,大体知道比如recen这个view是哪个方法调哪个方法最终加载出来,加载的关键代码在哪,点击事件怎么生成,而不在意里面的具体逻辑判断等等。

代码流程

1.systemui\src\com\android\systemui\statusbar\phone\statusbar.java;

从状态栏入口开始看。

protected void makestatusbarview() {
    final context context = mcontext;
    updatedisplaysize(); // populates mdisplaymetrics
    updateresources();
    updatetheme();

    ...
    ...

     try {
        boolean shownav = mwindowmanagerservice.hasnavigationbar();
        if (debug) log.v(tag, "hasnavigationbar=" + shownav);
        if (shownav) {
            createnavigationbar();//创建导航栏
        }
    } catch (remoteexception ex) {

    }
}

2.进入 createnavigationbar 方法,发现主要是用 navigationbarfragment 来管理.

protected void createnavigationbar() {
    mnavigationbarview = navigationbarfragment.create(mcontext, (tag, fragment) -> {
        mnavigationbar = (navigationbarfragment) fragment;
        if (mlightbarcontroller != null) {
            mnavigationbar.setlightbarcontroller(mlightbarcontroller);
        }
        mnavigationbar.setcurrentsysuivisibility(msystemuivisibility);
    });
}

3.看 navigationbarfragment 的create方法,终于知道,是windowmanager去addview了导航栏的布局,最终add了fragment的oncreateview加载的布局。(其实systemui所有的模块都是windowmanager来加载view)

public static view create(context context, fragmentlistener listener) {
    windowmanager.layoutparams lp = new windowmanager.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
                    | windowmanager.layoutparams.flag_split_touch
                    | windowmanager.layoutparams.flag_slippery,
            pixelformat.translucent);
    lp.token = new binder();
    lp.settitle("navigationbar");
    lp.windowanimations = 0;

    view navigationbarview = layoutinflater.from(context).inflate(
            r.layout.navigation_bar_window, null);

    if (debug) log.v(tag, "addnavigationbar: about to add " + navigationbarview);
    if (navigationbarview == null) return null;

    context.getsystemservice(windowmanager.class).addview(navigationbarview, lp);
    fragmenthostmanager fragmenthost = fragmenthostmanager.get(navigationbarview);
    navigationbarfragment fragment = new navigationbarfragment();
    fragmenthost.getfragmentmanager().begintransaction()
            .replace(r.id.navigation_bar_frame, fragment, tag) //注意!fragment里oncreateview加载的布局是add到这个window属性的view里的。
            .commit();
    fragmenthost.addtaglistener(tag, listener);
    return navigationbarview;
    }
}

4.systemui\res\layout\navigation_bar_window.xml

来看windowmanager加载的这个view的布局:navigation_bar_window.xml,发现根布局是自定义的view类navigationbarframe.(其实systemui以及其他系统应用如launcher,都是这种自定义view的方式,好多逻辑处理也都是在自定义view里,不能忽略)

<com.android.systemui.statusbar.phone.navigationbarframe
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:systemui="http://schemas.android.com/apk/res-auto"
    android:id="@+id/navigation_bar_frame"
    android:layout_height="match_parent"
    android:layout_width="match_parent">    

</com.android.systemui.statusbar.phone.navigationbarframe>

5.systemui\src\com\android\systemui\statusbar\phone\navigationbarframe.java

我们进入navigationbarframe类。发现类里并不是我们的预期,就是一个framelayout,对deadzone功能下的touch事件做了手脚,不管了。

6.再回来看看navigationbarfragment的生命周期呢。oncreateview()里,导航栏的真正的rootview。

@override
public view oncreateview(layoutinflater inflater, @nullable viewgroup container,
        bundle savedinstancestate) {
    return inflater.inflate(r.layout.navigation_bar, container, false);
}

进入导航栏的真正根布局:navigation_bar.xml,好吧又是自定义view,navigationbarview 和 navigationbarinflaterview 都要仔细研读。

<com.android.systemui.statusbar.phone.navigationbarview
    xmlns: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.navigationbarinflaterview
        android:id="@+id/navigation_inflater"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</com.android.systemui.statusbar.phone.navigationbarview>

7.systemui\src\com\android\systemui\statusbar\phone\navigationbarinflaterview.java;继承自framelayout

先看构造方法,因为加载xml布局首先走的是初始化

public navigationbarinflaterview(context context, attributeset attrs) {
    super(context, attrs);
    createinflaters();//根据屏幕旋转角度创建子view(单个back home or recent)的父布局
    display display = ((windowmanager)
            context.getsystemservice(context.window_service)).getdefaultdisplay();
    mode displaymode = display.getmode();
    isrot0landscape = displaymode.getphysicalwidth() > displaymode.getphysicalheight();
}
private void inflatechildren() {
    removeallviews();
    mrot0 = (framelayout) mlayoutinflater.inflate(r.layout.navigation_layout, this, false);
    mrot0.setid(r.id.rot0);
    addview(mrot0);
    mrot90 = (framelayout) mlayoutinflater.inflate(r.layout.navigation_layout_rot90, this, false);
    mrot90.setid(r.id.rot90);
    addview(mrot90);
    updatealternativeorder();
}

再看onfinishinflate()方法,这是view的生命周期,每个view被inflate之后都会回调。

@override
protected void onfinishinflate() {
    super.onfinishinflate();
    inflatechildren();//进去看无关紧要 忽略
    clearviews();//进去看无关紧要 忽略
    inflatelayout(getdefaultlayout());//关键方法:加载了 back.home.recent三个按钮的layout
}

看inflatelayout():里面的newlayout参数很重要!!!根据上一个方法看到getdefaultlayout(),他return了一个在xml写死的字符串。再看inflatelayout方法,他解析分割了xml里配置的字符串,并传给了inflatebuttons方法

protected void inflatelayout(string newlayout) {
    mcurrentlayout = newlayout;
    if (newlayout == null) {
        newlayout = getdefaultlayout();
    }
    string[] sets = newlayout.split(gravity_separator, 3);//根据“;”号分割成长度为3的数组
    string[] start = sets[0].split(button_separator);//根据“,”号分割,包含 left[.5w]和back[1wc]
    string[] center = sets[1].split(button_separator);//包含home
    string[] end = sets[2].split(button_separator);//包含recent[1wc]和right[.5w]
    // inflate these in start to end order or accessibility traversal will be messed up.
    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);
}

    protected string getdefaultlayout() {
    return mcontext.getstring(r.string.config_navbarlayout);
}

systemui\res\values\config.xml

 <!-- nav bar button default ordering/layout -->
<string name="config_navbarlayout" translatable="false">left[.5w],back[1wc];home;recent[1wc],right[.5w]</string>

再看inflatebuttons()方法,遍历加载inflatebutton:

private void inflatebuttons(string[] buttons, viewgroup parent, boolean landscape,
        boolean start) {
    for (int i = 0; i < buttons.length; i++) {
        inflatebutton(buttons[i], parent, landscape, start);
    }
}

@nullable
protected view inflatebutton(string buttonspec, viewgroup parent, boolean landscape,
        boolean start) {
    layoutinflater inflater = landscape ? mlandscapeinflater : mlayoutinflater;
    view v = createview(buttonspec, parent, inflater);//创建view
    if (v == null) return null;

    v = applysize(v, buttonspec, landscape, start);
    parent.addview(v);//addview到父布局
    addtodispatchers(v);
    view lastview = landscape ? mlastlandscape : mlastportrait;
    view accessibilityview = v;
    if (v instanceof reverseframelayout) {
        accessibilityview = ((reverseframelayout) v).getchildat(0);
    }
    if (lastview != null) {
        accessibilityview.setaccessibilitytraversalafter(lastview.getid());
    }
    if (landscape) {
        mlastlandscape = accessibilityview;
    } else {
        mlastportrait = accessibilityview;
    }
    return v;
}

我们来看createview()方法:以home按键为例,加载了home的button,其实是加载了 r.layout.home 的layout布局

private view createview(string buttonspec, viewgroup parent, layoutinflater inflater) {
    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);
    } else if (menu_ime.equals(button)) {
        v = inflater.inflate(r.layout.menu_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;
}

//systemui\res\layout\home.xml 
//这里布局里没有src显示home的icon,肯定是在代码里设置了
//这里也是自定义view:keybuttonview
<com.android.systemui.statusbar.policy.keybuttonview
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"//引用了dimens.xml里的navigation_key_width
android:layout_height="match_parent"
android:layout_weight="0"
systemui:keycode="3"//systemui自定义的属性
android:scaletype="fitcenter"
android:contentdescription="@string/accessibility_home"
android:paddingtop="@dimen/home_padding"
android:paddingbottom="@dimen/home_padding"
android:paddingstart="@dimen/navigation_key_padding"
android:paddingend="@dimen/navigation_key_padding"/>

8.systemui\src\com\android\systemui\statusbar\policy\keybuttonview.java

先来看keybuttonview的构造方法:我们之前xml的systemui:keycode=”3”方法在这里获取。再来看touch事件,通过sendevent()方法可以看出,back等view的点击touch事件不是自己处理的,而是交由系统以实体按键(keycode)的形式处理的.

当然keybuttonview类还处理了支持长按的button,按键的响声等,这里忽略。

至此,导航栏按键事件我们梳理完毕。

public keybuttonview(context context, attributeset attrs, int defstyle) {
    super(context, attrs);

    typedarray a = context.obtainstyledattributes(attrs, r.styleable.keybuttonview,
            defstyle, 0);

    mcode = a.getinteger(r.styleable.keybuttonview_keycode, 0);

    msupportslongpress = a.getboolean(r.styleable.keybuttonview_keyrepeat, true);
    mplaysounds = a.getboolean(r.styleable.keybuttonview_playsound, true);

    typedvalue value = new typedvalue();
    if (a.getvalue(r.styleable.keybuttonview_android_contentdescription, value)) {
        mcontentdescriptionres = value.resourceid;
    }

    a.recycle();

    setclickable(true);
    mtouchslop = viewconfiguration.get(context).getscaledtouchslop();
    maudiomanager = (audiomanager) context.getsystemservice(context.audio_service);

    mripple = new keybuttonripple(context, this);
    setbackground(mripple);
}

...
...

public boolean ontouchevent(motionevent ev) {

   ...

    switch (action) {
        case motionevent.action_down:
            mdowntime = systemclock.uptimemillis();
            mlongclicked = false;
            setpressed(true);
            if (mcode != 0) {
                sendevent(keyevent.action_down, 0, mdowntime);//关键方法
            } else {
                // provide the same haptic feedback that the system offers for virtual keys.
                performhapticfeedback(hapticfeedbackconstants.virtual_key);
            }
            playsoundeffect(soundeffectconstants.click);
            removecallbacks(mchecklongpress);
            postdelayed(mchecklongpress, viewconfiguration.getlongpresstimeout());
            break;

        ...
        ...

    }

    return true;
}

void sendevent(int action, int flags, long when) {
    mmetricslogger.write(new logmaker(metricsevent.action_nav_button_event)
            .settype(metricsevent.type_action)
            .setsubtype(mcode)
            .addtaggeddata(metricsevent.field_nav_action, action)
            .addtaggeddata(metricsevent.field_flags, flags));
    final int repeatcount = (flags & keyevent.flag_long_press) != 0 ? 1 : 0;
    //这里根据mcode new了一个keyevent事件,通过injectinputevent使事件生效。
    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);
    inputmanager.getinstance().injectinputevent(ev,
            inputmanager.inject_input_event_mode_async);
}

9.还遗留一个问题:设置图片的icon到底在哪?我们之前一直阅读的是navigationbarinflaterview,根据布局我们还有一个类没有看,navigationbarview.java

systemui\src\com\android\systemui\statusbar\phone\navigationbarview.java

进入navigationbarview类里,找到构造方法。

public navigationbarview(context context, attributeset attrs) {
    super(context, attrs);

    mdisplay = ((windowmanager) context.getsystemservice(
            context.window_service)).getdefaultdisplay();


    ...
    ...

    updateicons(context, configuration.empty, mconfiguration);//关键方法

    mbartransitions = new navigationbartransitions(this);

    //mbuttondispatchers 是维护这些home back recent图标view的管理类,会传递到他的child,navigationbarinflaterview类中
    mbuttondispatchers.put(r.id.back, new buttondispatcher(r.id.back));
    mbuttondispatchers.put(r.id.home, new buttondispatcher(r.id.home));
    mbuttondispatchers.put(r.id.recent_apps, new buttondispatcher(r.id.recent_apps));
    mbuttondispatchers.put(r.id.menu, new buttondispatcher(r.id.menu));
    mbuttondispatchers.put(r.id.ime_switcher, new buttondispatcher(r.id.ime_switcher));
    mbuttondispatchers.put(r.id.accessibility_button,new buttondispatcher(r.id.accessibility_button));

}

 private void updateicons(context ctx, configuration oldconfig, configuration newconfig) {

       ...

        iconlight = mnavbarplugin.gethomeimage(
                                    ctx.getdrawable(r.drawable.ic_sysbar_home));
        icondark = mnavbarplugin.gethomeimage(
                                    ctx.getdrawable(r.drawable.ic_sysbar_home_dark));
        //mhomedefaulticon = getdrawable(ctx,
        //        r.drawable.ic_sysbar_home, r.drawable.ic_sysbar_home_dark);
        mhomedefaulticon = getdrawable(iconlight,icondark);

        //亮色的icon资源
        iconlight = mnavbarplugin.getrecentimage(
                                    ctx.getdrawable(r.drawable.ic_sysbar_recent));
        //暗色的icon资源
        icondark = mnavbarplugin.getrecentimage(
                                    ctx.getdrawable(r.drawable.ic_sysbar_recent_dark));
        //mrecenticon = getdrawable(ctx,
        //        r.drawable.ic_sysbar_recent, r.drawable.ic_sysbar_recent_dark);
        mrecenticon = getdrawable(iconlight,icondark);


        mmenuicon = getdrawable(ctx, r.drawable.ic_sysbar_menu,
                                    r.drawable.ic_sysbar_menu_dark);

       ...
       ...

}

10.从第10可以看到,以recent为例,在初始化时得到了mrecenticon的资源,再看谁调用了了mrecenticon就可知道,即反推看调用流程。

private void updaterecentsicon() {
    getrecentsbutton().setimagedrawable(mdockedstackexists ? mdockedicon : mrecenticon);
    mbartransitions.reapplydarkintensity();
}

updaterecentsicon这个方法设置了recent图片的资源,再看谁调用了updaterecentsicon方法:onconfigurationchanged屏幕旋转会重新设置资源图片

@override
protected void onconfigurationchanged(configuration newconfig) {
    super.onconfigurationchanged(newconfig);
    boolean uicarmodechanged = updatecarmode(newconfig);
    updatetaskswitchhelper();
    updateicons(getcontext(), mconfiguration, newconfig);
    updaterecentsicon();
    if (uicarmodechanged || mconfiguration.densitydpi != newconfig.densitydpi
            || mconfiguration.getlayoutdirection() != newconfig.getlayoutdirection()) {
        // if car mode or density changes, we need to reset the icons.
        setnavigationiconhints(mnavigationiconhints, true);
    }
    mconfiguration.updatefrom(newconfig);
}

public void setnavigationiconhints(int hints, boolean force) {

    ...
    ...

    mnavigationiconhints = hints;

    // we have to replace or restore the back and home button icons when exiting or entering
    // carmode, respectively. recents are not available in carmode in nav bar so change
    // to recent icon is not required.
    keybuttondrawable backicon = (backalt)
            ? getbackiconwithalt(musecarmodeui, mvertical)
            : getbackicon(musecarmodeui, mvertical);

    getbackbutton().setimagedrawable(backicon);

    updaterecentsicon();

    ...
    ...

}

reorient()也调用了setnavigationiconhints()方法:

public void reorient() {
    updatecurrentview();

    ...

    setnavigationiconhints(mnavigationiconhints, true);

    gethomebutton().setvertical(mvertical);
}

再朝上推,最终追溯到navigationbarfragment的onconfigurationchanged()方法 和 navigationbarview的onattachedtowindow()和onsizechanged()方法。也就是说,在navigationbarview导航栏这个布局加载的时候就会设置图片资源,和长度改变,屏幕旋转都有可能引起重新设置

至此,systemui的虚拟导航栏模块代码流程结束。

总结

  1. 创建一个window属性的父view
  2. 通过读取解析xml里config的配置,addview需要的icon,或者调换顺序
  3. src图片资源通过代码设置亮色和暗色
  4. touch事件以keycode方式交由系统处理

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网