SystemUI之KeyguardSliceView

什么是Slice

什么是Slice?Slice是今年Goole I/O者大会随Android P一起推出的一大新特性,这个功能可以让您的应用以模块化、富交互的形式插入到多个使用场景中,比如 Google Search 和 Assistant。Slices 支持的交互包括 actions、开关、滑动条、滑动内容等等。

SystemUI中的Slice应用

在Android P中SystemUI模块也运用到了Slice新特性,主要表现在锁屏上时间下面的日期、勿扰图标以及闹钟等展示,效果如下:


keyguardsliceview.png

如何使用Slice

接下来通过KeyguardSliceView来讲讲如何使用这个Slice。

添加依赖

一个新的特性在工程中是需要增加对应的依赖的,这里可以看到SystemUI的mk文件:

LOCAL_STATIC_ANDROID_LIBRARIES := \
    Mtk-SystemUIPluginLib \
    Mtk-SystemUISharedLib \
    android-support-car \
    android-support-v4 \
    android-support-v7-recyclerview \
    android-support-v7-preference \
    android-support-v7-appcompat \
    android-support-v7-mediarouter \
    android-support-v7-palette \
    android-support-v14-preference \
    android-support-v17-leanback \
    android-slices-core \
    android-slices-view \
    android-slices-builders \
    android-arch-core-runtime \
    android-arch-lifecycle-extensions \

如果是在Android Studio中new的工程的话,那么需要在gradle中增加以下依赖:

implementation 'androidx.slice:slice-core:1.0.0'
implementation 'androidx.slice:slice-builders:1.0.0'
implementation 'androidx.slice:slice-view:1.0.0'

注册provider

前面说到,Slice是一个集合其他模块一起展示的功能,那么如何实现跨应用展示某个App的数据呢?这里就是通过provider来更新Slice,所以首先需要在AndroidManifest中注册一个provider:

<provider android:name=".keyguard.KeyguardSliceProvider"
                  android:authorities="com.android.systemui.keyguard"
                  android:grantUriPermissions="true"
                  android:exported="true">
</provider>

这个provider对应了源码中的KeyguardSliceProvider.java。KeyguardSliceProvider继承了SliceProvider,而SliceProvider继承了ContentProvider。这里需要关注两个函数onCreateSliceProvider()、onBindSlice()。onCreateSliceProvider()是SliceProvider初始化的时候调用的:

@Override
public final boolean onCreate() {
        if (!BuildCompat.isAtLeastP()) {
            mCompat = new SliceProviderCompat(this,
                    onCreatePermissionManager(mAutoGrantPermissions), getContext());
        }
        return onCreateSliceProvider();
}

在KeyguardSliceProvider中onCreateSliceProvider(),对需要监听的模块进行初始化:

@Override
public boolean onCreateSliceProvider() {
        mAlarmManager = getContext().getSystemService(AlarmManager.class);
        mContentResolver = getContext().getContentResolver();
        mNextAlarmController = new NextAlarmControllerImpl(getContext());
        mNextAlarmController.addCallback(this);
        mZenModeController = new ZenModeControllerImpl(getContext(), mHandler);
        mZenModeController.addCallback(this);
        mDatePattern = getContext().getString(R.string.system_ui_aod_date_pattern);
        mPendingIntent = PendingIntent.getActivity(getContext(), 0, new Intent(), 0);
        KeyguardSliceProvider.sInstance = this;
        registerClockUpdate();
        updateClock();
        return true;
}

注册之后是怎么进行刷新的呢?既然是provider,那么肯定有地方进行notifyChange的,通过对该类的阅读,发现在注册的一些callback中就有notifyChange,如下面的ZenMode:

@Override
public void onZenChanged(int zen) {
        mContentResolver.notifyChange(mSliceUri, null /* observer */);
}

那么在notifyChange之后,接下来就会进行刚刚说到的onBindSlice():

@Override
public Slice onBindSlice(Uri sliceUri) {
        Trace.beginSection("KeyguardSliceProvider#onBindSlice");
        ListBuilder builder = new ListBuilder(getContext(), mSliceUri);
        builder.addRow(new RowBuilder(builder, mDateUri).setTitle(mLastText));
        addNextAlarm(builder);
        addZenMode(builder);
        addPrimaryAction(builder);
        Slice slice = builder.build();
        Trace.endSection();
        return slice;
}

和Notification类似,这里通过ListBuilder设置Tile或者icon。到这里,Slice的数据加载流程已经实现了。接下来就讲讲如何将封装好的slice显示在Keyguard上。

KeyguardSliceView

前面讲的是数据加载的流程,接下来就讲讲UI是如何显示刷新,SystemUI中通过自定义View——KeyguardSliceView显示Slice的,首先来看一下自定义View的布局:

<com.android.keyguard.KeyguardSliceView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_marginStart="16dp"
    android:layout_marginEnd="16dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:clipToPadding="false"
    android:orientation="vertical"
    android:layout_centerHorizontal="true">
    <view class="com.android.keyguard.KeyguardSliceView$TitleView"
              android:id="@+id/title"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginBottom="@dimen/widget_title_bottom_margin"
              android:paddingStart="64dp"
              android:paddingEnd="64dp"
              android:visibility="gone"
              android:textColor="?attr/wallpaperTextColor"
              android:theme="@style/TextAppearance.Keyguard"
    />
    <view class="com.android.keyguard.KeyguardSliceView$Row"
              android:id="@+id/row"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:orientation="horizontal"
              android:gravity="left"
              android:layout_marginLeft="10dp"
    />
</com.android.keyguard.KeyguardSliceView>

从布局中来看,KeyguardSliceView是由两个内部自定义View控件TitleView和Row组成的,这里主要关注Row这个子控件,锁屏显示的日期以及闹钟勿扰等图标都是在Row中加载的。

如何监听Slice

那么如何监听Slice的变化刷新UI呢?这就得看下KeyguardSliceView的初始化流程了,因为KeyguardSliceView implement了TunerService,所以初始化的时候就会调用onTuningChanged:

@Override
public void onTuningChanged(String key, String newValue) {
        setupUri(newValue);
    }

public void setupUri(String uriString) {
        Log.v(TAG, "uriString: " + uriString);
        if (uriString == null) {
            uriString = KeyguardSliceProvider.KEYGUARD_SLICE_URI;
        }

        boolean wasObserving = false;
        if (mLiveData != null && mLiveData.hasActiveObservers()) {
            wasObserving = true;
            mLiveData.removeObserver(this);
        }

        mKeyguardSliceUri = Uri.parse(uriString);
        mLiveData = SliceLiveData.fromUri(mContext, mKeyguardSliceUri);

        if (wasObserving) {
            mLiveData.observeForever(this);
        }
    }

默认的uriString是null,这样就将KeyguardProvider的KEYGUARD_SLICE_URI保存到了mLiveData,这里用到的LiveData

LiveData

LiveData是一个可被观察的数据持有类,不同于其他Observer,LiveData是对生命周期有感知的,它会遵循App组件的生命周期,如Activity,Fragment,Service等。具有以下优点:

  • 实时同步UI和数据
    LiveData采用了观察者模式设计,其中LiveData是被观察者,当数据发生变化时会通知观察者进行数据更新。通过这点,可以确保数据和界面的实时性。
  • 有效规避内存泄露
    这是因为LiveData能够感知到组件的生命周期,当组件状态处于DESTROYED状态时,观察者对象会被remove。
  • 不会因为组件销毁导致崩溃
    这是因为组件处于非激活状态时,在界面不会收到来自LiveData的数据变化通知。这样规避了很多因为页面销毁之后,修改UI导致的crash。
  • 无需手动处理生命周期
    LiveData能够感知组件的生命周期,无需设置LiveData组件的生命周期状态。
  • 始终保持数据更新
    生命周期从非活跃状态切换到活跃状态的时候,能够实时的接收最新的数据。
  • 不会因为配置变化导致数据丢失(onConfigChanged)
    由于LiveData保存数据的时候,组件和数据是分离的,所以在配置更改(如横竖屏切换等)的时候,即便组件被重新创建,因为数据还保存在LiveData中,这样也能够做到实时的更新。
  • 资源共享
    单例模式扩展LiveData对象并包装成系统服务,以便在应用程序中进行共享,需要该资源的只需要观察LiveData即可。

注册与反注册

这里通过自定义View的onAttachedToWindow与onDetachedFromWindow进行注册监听和反注册:

@Override
protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        Log.v(TAG, "onAttachedToWindow");

        // Make sure we always have the most current slice
        mLiveData.observeForever(this);
        Dependency.get(ConfigurationController.class).addCallback(this);
}

@Override
protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        mLiveData.removeObserver(this);
        Dependency.get(ConfigurationController.class).removeCallback(this);
}

因为自定义View implement了Observer<Slice>,所以监听的KeyguardSliceProvider一旦发生变化,就会调用onChange:

/**
  *  observer lifecycle.
  * @param slice the new slice content.
  */
@Override
public void onChanged(Slice slice) {
        Log.v(TAG, "onChanged");
        mSlice = slice;
        showSlice();
}

从而触发自定义View UI显示逻辑。

显示

显示逻辑主要在showSlice()中:

private void showSlice() {
        Trace.beginSection("KeyguardSliceView#showSlice");
        Log.v(TAG, "showSlice");
        if (mPulsing || mSlice == null) {
            mTitle.setVisibility(GONE);
            mRow.setVisibility(GONE);
            if (mContentChangeListener != null) {
                mContentChangeListener.run();
            }
            return;
        }

        ListContent lc = new ListContent(getContext(), mSlice);
        mHasHeader = lc.hasHeader();
        List<SliceItem> subItems = new ArrayList<SliceItem>();
        for (int i = 0; i < lc.getRowItems().size(); i++) {
            SliceItem subItem = lc.getRowItems().get(i);
            String itemUri = subItem.getSlice().getUri().toString();
            // Filter out the action row
            if (!KeyguardSliceProvider.KEYGUARD_ACTION_URI.equals(itemUri)) {
                subItems.add(subItem);
            }
        }
        if (!mHasHeader) {
            mTitle.setVisibility(GONE);
        } else {
            mTitle.setVisibility(VISIBLE);

            // If there's a header it'll be the first subitem
            RowContent header = new RowContent(getContext(), subItems.get(0),
                    true /* showStartItem */);
            SliceItem mainTitle = header.getTitleItem();
            CharSequence title = mainTitle != null ? mainTitle.getText() : null;
            mTitle.setText(title);
        }

        mClickActions.clear();
        final int subItemsCount = subItems.size();
        final int blendedColor = getTextColor();
        final int startIndex = mHasHeader ? 1 : 0; // First item is header; skip it
        mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE);
        for (int i = startIndex; i < subItemsCount; i++) {
            SliceItem item = subItems.get(i);
            RowContent rc = new RowContent(getContext(), item, true /* showStartItem */);
            final Uri itemTag = item.getSlice().getUri();
            // Try to reuse the view if already exists in the layout
            KeyguardSliceButton button = mRow.findViewWithTag(itemTag);
            if (button == null) {
                button = new KeyguardSliceButton(mContext);
                button.setTextColor(blendedColor);
                button.setTag(itemTag);
                final int viewIndex = i - (mHasHeader ? 1 : 0);
                mRow.addView(button, viewIndex);
            }

            PendingIntent pendingIntent = null;
            if (rc.getPrimaryAction() != null) {
                pendingIntent = rc.getPrimaryAction().getAction();
            }
            mClickActions.put(button, pendingIntent);

            final SliceItem titleItem = rc.getTitleItem();
            button.setText(titleItem == null ? null : titleItem.getText());
            button.setContentDescription(rc.getContentDescription());

            Drawable iconDrawable = null;
            SliceItem icon = SliceQuery.find(item.getSlice(),
                    android.app.slice.SliceItem.FORMAT_IMAGE);
            if (icon != null) {
                iconDrawable = icon.getIcon().loadDrawable(mContext);
                final int width = (int) (iconDrawable.getIntrinsicWidth()
                        / (float) iconDrawable.getIntrinsicHeight() * mIconSize);
                iconDrawable.setBounds(0, 0, Math.max(width, 1), mIconSize);
            }
            button.setCompoundDrawables(iconDrawable, null, null, null);
            button.setOnClickListener(this);
            button.setClickable(pendingIntent != null);
        }

        // Removing old views
        for (int i = 0; i < mRow.getChildCount(); i++) {
            View child = mRow.getChildAt(i);
            if (!mClickActions.containsKey(child)) {
                mRow.removeView(child);
                i--;
            }
        }

        if (mContentChangeListener != null) {
            mContentChangeListener.run();
        }
        Trace.endSection();
    }

这里主要就是对onChange中的slice进行解析,并将Slice中携带的内容,封装到KeyguardSliceButton,然后一一add到Row中。这里的KeyguardSliceButton也是一个自定义控件,用来存储slice中的文字与图片。这里还需要注意的是,每一个Slice都是可以进行跳转的,类似于Notification,上面代码中就有此功能的逻辑体现:

PendingIntent pendingIntent = null;
if (rc.getPrimaryAction() != null) {
       pendingIntent = rc.getPrimaryAction().getAction();
}
mClickActions.put(button, pendingIntent);

如果slice在创建的时候携带pendingIntent,那么点击后就会启动对应的app或者响应的操作。
到这里,KeyguardSliceView的数据加载流程和UI流程已经讲完,若有不正确的地方请指,后面也会对Sliceprovider相关源码进一步的进行分析,敬请期待:)

本文已独家授予微信公众号ApeClub!转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容