什么是Slice
什么是Slice?Slice是今年Goole I/O者大会随Android P一起推出的一大新特性,这个功能可以让您的应用以模块化、富交互的形式插入到多个使用场景中,比如 Google Search 和 Assistant。Slices 支持的交互包括 actions、开关、滑动条、滑动内容等等。
SystemUI中的Slice应用
在Android P中SystemUI模块也运用到了Slice新特性,主要表现在锁屏上时间下面的日期、勿扰图标以及闹钟等展示,效果如下:
如何使用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!转载请注明出处。