最近发现Android P上锁屏界面,日期不显示,发现从P开始后,出现了Slice来允许应用以模块化,可交互的方式,插入多个使用场景。Android P的system UI 也使用到了这一特性,表现为锁屏时间,日期,勿扰图标,闹钟等。
首先,我们打开System UI 的mk 文件,可以看到以下代码:
LOCAL_STATIC_ANDROID_LIBRARIES := \ SystemUIPluginLib \ 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-slices-core, view ,builders,就是实现Slice特性的依赖,至于Android studio工程,则需要在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'Slice 是一个集合其他模块,共同展示信息的功能,为了实现跨进程间的信息传递,它采用了provider来更新slice,而contentProvider需要在Manifest中注册,如SystemUI:
<provider android:name=".keyguard.KeyguardSliceProvider" android:authorities="com.android.systemui.keyguard" android:grantUriPermissions="true" android:exported="true"> </provider>通过查看KeyguardSliceProvider.java,可以看到它继承了 SliceProvider,而SliceProvider又继承了ContentProvider。这里关注两个函数onCreateSliceProvider()、onBindSlice()。onCreateSliceProvider()是SliceProvider初始化的时候调用的:
@Override public final boolean onCreate() { if (!BuildCompat.isAtLeastP()) { mCompat = new SliceProviderCompat(this, onCreatePermissionManager(mAutoGrantPermissions), getContext()); } return onCreateSliceProvider(); }o’nCreateSliceProvider中做了如下的操作:
@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; }主要是对几个需要显示的模块的监听初始化,包括Alarm,ZenMode,Clock; 注册之后是怎么进行刷新的呢?既然是provider,那么肯定有地方进行notifyChange的,通过对该类的阅读,发现在注册的一些callback中就有notifyChange,如下面的ZenMode:
public class KeyguardSliceProvider extends SliceProvider implements NextAlarmController.NextAlarmChangeCallback, ZenModeController.Callback复写了ZenMode的onZenChanged,
@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的创建类似,就是各个模块传递过来的icon(Row),title之类的信息。
SystemUi从KeyguardSliceProvider获取数据,那么如何刷新界面呢,其主要操作就是在KeyguardSliceView这里。 在KeyguardStatusView.java中:
mKeyguardSlice = findViewById(R.id.keyguard_status_area); mKeyguardSlice.setContentChangeListener(this::onSliceContentChanged);接下来看keyguard_status_area的布局:
<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="center" /> </com.android.keyguard.KeyguardSliceView>可以看到其包含了两个自定义控件,着重关注Row控件,日期,闹钟,勿扰等图标都是在Row中的。
Slice是如何监听,并且刷新到UI的呢,我们从KeyguardSliceView的初始化看起,可以看到,它实现了 TunerService.Tunable接口,所以初始化的时候就会调用onTuningChanged:
@Override public void onTuningChanged(String key, String newValue) { setupUri(newValue); } public void setupUri(String 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); } }这里要重点注意,和bug有关,uriString 在KeyguardSliceView初始化时,默认是null,但会立刻被填充 KeyguardSliceProvider.KEYGUARD_SLICE_URI,随后,会被保存到mLiveData中。
private LiveData mLiveData; 可以看到mLiveData是一个slice 集合类,Android 给它的解释是LiveData is a data holder class that can be observed within a given lifecycle.,意思是LiveData是一个可被观察的数据持有类,不同于其他Observer,LiveData是对生命周期有感知的,它会遵循App组件的生命周期,如Activity,Fragment,Service等。 它的优点在于:
实时同步UI和数据 LiveData采用了观察者模式设计,其中LiveData是被观察者,当数据发生变化时会通知观察者进行数据更新。通过这点,可以确保数据和界面的实时性。有效规避内存泄露 这是因为LiveData能够感知到组件的生命周期,当组件状态处于DESTROYED状态时,观察者对象会被remove。不会因为组件销毁导致崩溃 这是因为组件处于非激活状态时,在界面不会收到来自LiveData的数据变化通知。这样规避了很多因为页面销毁之后,修改UI导致的crash。无需手动处理生命周期 LiveData能够感知组件的生命周期,无需设置LiveData组件的生命周期状态。始终保持数据更新 生命周期从非活跃状态切换到活跃状态的时候,能够实时的接收最新的数据。不会因为配置变化导致数据丢失(onConfigChanged) 由于LiveData保存数据的时候,组件和数据是分离的,所以在配置更改(如横竖屏切换等)的时候,即便组件被重新创建,因为数据还保存在LiveData中,这样也能够做到实时的更新。资源共享 单例模式扩展LiveData对象并包装成系统服务,以便在应用程序中进行共享,需要该资源的只需要观察LiveData即可。它的注册和反注册:
* * @param observer The observer that will receive the events */ @MainThread public void observeForever(@NonNull Observer<? super T> observer) { assertMainThread("observeForever"); AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer); ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper); if (existing != null && existing instanceof LiveData.LifecycleBoundObserver) { throw new IllegalArgumentException("Cannot add the same observer" + " with different lifecycles"); } if (existing != null) { return; } wrapper.activeStateChanged(true); } /** * Removes the given observer from the observers list. * * @param observer The Observer to receive events. */ @MainThread public void removeObserver(@NonNull final Observer<? super T> observer) { assertMainThread("removeObserver"); ObserverWrapper removed = mObservers.remove(observer); if (removed == null) { return; } removed.detachObserver(); removed.activeStateChanged(false); }上面提到,Livedata感知生命周期,因此它的注册与反注册需要跟着观察者的生命周期走,如KeyguardSliceView中,它的注册与反注册,就是在onAttachedToWindow() 和onDetachedFromWindow() 中处理的,如下:
@Override protected void onAttachedToWindow() { super.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); }因KeyguardSliceView实现了Observer,所以一旦slice控件发生变化,就会回调到onChange(),从而触发刷新页面。
/** * LiveData observer lifecycle. * @param slice the new slice content. */ @Override public void onChanged(Slice slice) { mSlice = slice; showSlice(); }最后看showSlice()的代码:
private void showSlice() { Trace.beginSection("KeyguardSliceView#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++) { //逐条解析mSlice中的slice 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); //如果KeyguardSliceProvider中存在相应的Uri,上面有提及初始化的情况,个人理解就是需要显示slice控件的app,需要在provider中注册 } } 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); //将获取的URi解析后,逐条填充到KeyguardSliceButton,这也是一个自定义控件 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); //这里填充slice icon,就是我们见到的显示的slice 图标,如闹钟。 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(); }整个流程分析结束,现在回过头来看bug:为什么锁屏上的date会消失呢。
看上方showSlice()代码,mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE);,我们在前文看到,mRow实际上就是显示日期,闹钟,勿扰图标的,那么在不显示的情况下,可以确定subItemsCount = 0的,那么往上追,它的count值,是由onChange时,获取到的mLiveDate的条目数决定的,而liveDate中的slice,是由StringUri解析而来:
mKeyguardSliceUri = Uri.parse(uriString); mLiveData = SliceLiveData.fromUri(mContext, mKeyguardSliceUri);因此,可以确定,在某种情况下, mKeyguardSliceUri 解析出了问题,此时,我们打印出 mKeyguardSliceUri 的值,发现值为:
content://com.android.systemui.keyguard/main这个值,在KeyguardSliceProvider.java中,定义为:
public static final String KEYGUARD_SLICE_URI = "content://com.android.systemui.keyguard/main";这时,我们发现,KEYGUARD_SLICE_URI正是在setupUri时,默认填充了,所以Slice组件就认为此处无任何row图标,因此只要默认填充date的URI,就可以发现,每次date都是默认填充的了,查看KeyguardSliceProvider.java,它的值是:
public static final String KEYGUARD_DATE_URI = "content://com.android.systemui.keyguard/date";因此,代码修改为:
public void setupUri(String uriString) { if (uriString == null) { //uriString = KeyguardSliceProvider.KEYGUARD_SLICE_URI; uriString = KeyguardSliceProvider.KEYGUARD_DATE_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); } }编译后,问题解决。