美文网首页SystemUI
SystemUI之KeyguardSliceView

SystemUI之KeyguardSliceView

作者: Monster_de47 | 来源:发表于2018-12-14 13:50 被阅读661次

    什么是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!转载请注明出处。

    相关文章

      网友评论

        本文标题:SystemUI之KeyguardSliceView

        本文链接:https://www.haomeiwen.com/subject/jrqmhqtx.html