SystemUI

SystemUI之KeyguardSliceView

2018-12-14  本文已影响661人  Monster_de47

什么是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等。具有以下优点:

注册与反注册

这里通过自定义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!转载请注明出处。

上一篇下一篇

猜你喜欢

热点阅读