|
方案一、aar架包集成
最简单直接的方案,卡片侧实现,打成aar包提供到launcher显示
方案二、AppWidget
原生的桌面小组件方案,被限制无法自定义view
底层通过BroadcastReceiver实现
方案三、插件方案
插件方案有好几种,实现原理都是通过配置实现,其中有Service,BroadcastReceiver,Plugin
在SystemUI模块中,状态栏等模块很多使用的都是Plugin方案跟Service方案
这里详细讲通过Service配置跟Plugin配置实现
插件方案可以实现卡片跟launcher解耦,并且可以自定义view,还支持跨进程交互
首先定义一个插件,用于配置卡片信息,exported 属性标识可以给其它应用读取
<service
android:name=".TestWidgetService"
android:exported="true"
android:label="测试卡片1">
<intent-filter>
<action android:name="com.appwidget.action.rear.APPWIDGET_PLUGIN" />
</intent-filter>
<meta-data
android:name="com.appwidget.provider"
android:resource="@xml/remote_control_widget_info" />
</service>
<service
android:name=".PagerWidgetPlugin"
android:exported="true"
android:label="测试卡片2">
<intent-filter>
<action android:name="com.appwidget.action.rear.APPWIDGET_PLUGIN" />
</intent-filter>
<meta-data
android:name="com.appwidget.provider"
android:resource="@xml/pager_widget_info" />
</service>
View Code
package com.example.page
import android.content.Context
interface Plugin {
fun onCreate(hostContext: Context, pluginContext: Context) {
}
fun onDestroy() {
}
}
class PagerWidgetPlugin : Plugin
Plugin
package com.example.page
import android.app.Service
import android.content.Intent
import android.os.IBinder
class TestWidgetService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
}
Service
上面插件是直接定义在卡片里,其实应该在launcher中,然后对所有的卡片提供基础aar,统一接口
然后在res/xml下面新建 widget_info.xml
<?xml version="1.0" encoding="utf-8"?>
<com-appwidget-provider
cardType="0"
mediumLayout="@layout/pager_control_layout" />
pager_widget_info
<?xml version="1.0" encoding="utf-8"?>
<com-appwidget-provider
cardType="0"
smallLayout="@layout/cards_remote_control_layout" />
remote_control_widget_info
编写卡片布局
![]() ![]()
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:focusable="false">
<ImageView
android:id="@+id/card_remote_control_image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/card_remote_control_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="32dp"
android:drawableLeft="@mipmap/ic_launcher_round"
android:drawablePadding="8dp"
android:text="title"
android:textColor="@android:color/holo_blue_dark"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/card_remote_control_tips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:maxWidth="390dp"
android:singleLine="true"
android:text="tips"
android:textColor="@android:color/holo_orange_dark"
app:layout_constraintBottom_toTopOf="@+id/card_remote_control_summary"
app:layout_constraintStart_toStartOf="@+id/card_remote_control_summary" />
<TextView
android:id="@+id/card_remote_control_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginBottom="35dp"
android:ellipsize="end"
android:maxWidth="405dp"
android:singleLine="true"
android:text="content"
android:textColor="@android:color/holo_blue_bright"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
cards_remote_control_layout
<?xml version="1.0" encoding="utf-8"?>
<com.example.page.loop.CustomViewPager xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white" />
pager_control_layout
然后在launcher中,使用 AppWidgetManager 来读取配置信息
![]() ![]()
package com.test.launcher.rear.card.appwidget
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.util.Log
import com.blankj.utilcode.util.GsonUtils
import com.kunminx.architecture.ui.callback.UnPeekLiveData
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
@SuppressLint("StaticFieldLeak")
object AppWidgetManager {
val context: Context = android.app.AppGlobals.getInitialApplication()
private const val ACTION = "com.appwidget.action.rear.APPWIDGET_PLUGIN"
private const val META_DATA_APPWIDGET_PROVIDER: String = "com.appwidget.provider"
private val list = mutableListOf<CardModel>()
private var mAppWidgetChangeListener: ((MutableList<CardModel>) -> Unit)? = null
val showOnCards = UnPeekLiveData(mutableListOf<CardModel>())
init {
val intent = Intent(ACTION)
val resolveInfoList = context.packageManager.queryIntentServices(
intent,
PackageManager.GET_META_DATA or PackageManager.GET_SHARED_LIBRARY_FILES
)
Logger.d("resolveInfoList size ${resolveInfoList.size}")
resolveInfoList.forEach { ri ->
parseAppWidgetProviderInfo(ri)
}
}
var id = 0
fun allocateAppWidgetId(): Int {
return ++id
}
fun setAppWidgetChangeListener(listener: ((MutableList<CardModel>) -> Unit)?) {
mAppWidgetChangeListener = listener
}
private fun parseAppWidgetProviderInfo(resolveInfo: ResolveInfo) {
val componentName =
ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name)
val serviceInfo = resolveInfo.serviceInfo
val hasXmlDefinition = serviceInfo.metaData?.getInt(META_DATA_APPWIDGET_PROVIDER) != 0
if (hasXmlDefinition) {
val info = CardInfo()
info.serviceInfo = serviceInfo
info.componentName = componentName
val pm = context.packageManager
try {
serviceInfo.loadXmlMetaData(pm, META_DATA_APPWIDGET_PROVIDER).use { parser ->
if (parser == null) {
Logger.w("$componentName parser is null")
return
}
val nodeName: String = parser.name
if ("com-appwidget-provider" != nodeName) {
Logger.w("$componentName provider is null")
return
}
info.descriptionRes =
parser.getAttributeResourceValue(null, "description", 0)
info.mediumLayout =
parser.getAttributeResourceValue(null, "mediumLayout", 0)
info.mediumPreviewImage =
parser.getAttributeResourceValue(null, "mediumPreviewImage", 0)
info.smallLayout =
parser.getAttributeResourceValue(null, "smallLayout", 0)
if (info.smallLayout != 0) {
info.sizeStyle = 1
}
info.smallPreviewImage =
parser.getAttributeResourceValue(null, "smallPreviewImage", 0)
info.bigLayout =
parser.getAttributeResourceValue(null, "bigLayout", 0)
info.bigPreviewImage =
parser.getAttributeResourceValue(null, "bigPreviewImage", 0)
if (info.bigLayout != 0) {
info.sizeStyle = 2
}
Logger.d("parseAppWidgetProviderInfo $componentName hasLayout=${info.hasLayout()}")
if (info.hasLayout()) {
list.add(CardModel(allocateAppWidgetId(), info, false))
}
return
}
} catch (e: IOException) {
// Ok to catch Exception here, because anything going wrong because
// of what a client process passes to us should not be fatal for the
// system process.
Logger.e("XML parsing failed for AppWidget provider $componentName", e)
return
} catch (e: PackageManager.NameNotFoundException) {
Logger.e("XML parsing failed for AppWidget provider $componentName", e)
return
} catch (e: XmlPullParserException) {
Logger.e("XML parsing failed for AppWidget provider $componentName", e)
return
}
}
}
}
View Code
也可以通过加载器获取
private fun parseAppWidgetProviderInfo(resolveInfo: ResolveInfo) {
val componentName =
ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name)
val serviceInfo = resolveInfo.serviceInfo
val pluginContext = PluginContextWrapper.createFromPackage(serviceInfo.packageName)
try {
val cardPlugin = Class.forName(
serviceInfo.name, true, pluginContext.classLoader
).newInstance() as CardPlugin
cardPlugin.onCreate(context, pluginContext)
} catch (e: Exception) {
Log.w(TAG, "parseAppWidgetProviderInfo failed for AppWidget provider $componentName", e)
}
}
View Code
因为处于不用apk,所以加载卡片类,需要加载其他路径的类文件,需要把这个类文件路径加到自己的classloader
![]() ![]()
package com.test.carlauncher.cards.plugin
import android.app.Application
import android.content.Context
import android.content.ContextWrapper
import android.text.TextUtils
import android.view.LayoutInflater
import dalvik.system.PathClassLoader
import java.io.File
class PluginContextWrapper(
base: Context,
private val classLoader: ClassLoader = ClassLoaderFilter(base.classLoader)
) : ContextWrapper(base) {
private val application: Application by lazy {
PluginApplication(this)
}
private val mInflater: LayoutInflater by lazy {
LayoutInflater.from(baseContext).cloneInContext(this)
}
override fun getClassLoader(): ClassLoader {
return classLoader
}
override fun getApplicationContext(): Context {
return application
}
override fun getSystemService(name: String): Any {
if (LAYOUT_INFLATER_SERVICE == name) {
return mInflater
}
return baseContext.getSystemService(name)
}
override fun toString(): String {
return "${javaClass.name}@${Integer.toHexString(hashCode())}_$packageName"
}
companion object {
private val contextMap = mutableMapOf<String, Context>()
private val methodSetOuterContext = Class.forName("android.app.ContextImpl")
.getDeclaredMethod("setOuterContext", Context::class.java).apply {
isAccessible = true
}
private fun Context.setOuterContext(outContext: Context) {
methodSetOuterContext.invoke(this, outContext)
}
fun createFromPackage(packageName: String): Context {
val contextCache = contextMap.get(packageName)
if (contextCache != null) {
return contextCache
}
val hostContext: Context = android.app.AppGlobals.getInitialApplication()
val appInfo = hostContext.packageManager.getApplicationInfo(packageName, 0)
val appContext: Context = hostContext.createApplicationContext(
appInfo,
CONTEXT_INCLUDE_CODE or CONTEXT_IGNORE_SECURITY
)
val zipPaths = mutableListOf<String>()
val libPaths = mutableListOf<String>()
android.app.LoadedApk.makePaths(null, true, appInfo, zipPaths, libPaths);
val classLoader = PathClassLoader(
TextUtils.join(File.pathSeparator, zipPaths),
TextUtils.join(File.pathSeparator, libPaths),
ClassLoaderFilter(hostContext.classLoader)
)
// 注册广播、绑定服务、startActivity会使用OuterContext
// (appContext as android.app.ContextImpl).setOuterContext(context)
appContext.setOuterContext(hostContext)
return PluginContextWrapper(appContext, classLoader).also {
contextMap.put(packageName, it)
}
}
}
}
View Code
class ClassLoaderFilter(
private val mBase: ClassLoader,
private val mPackages: Array<String>
) : ClassLoader(getSystemClassLoader()) {
@Throws(ClassNotFoundException::class)
override fun loadClass(name: String, resolve: Boolean): Class<*> {
for (pkg in mPackages) {
if (name.startsWith(pkg)) {
return mBase.loadClass(name)
}
}
return super.loadClass(name, resolve)
}
}
View Code
class PluginApplication(context: Context) : Application() {
init {
attachBaseContext(context)
}
}
View Code
获取到卡片的context跟classloader后,传入到 PluginContextWrapper 中,用于后续卡片内加载布局
通过PathClassLoader构建的类加载器包含了插件APK的路径,当调用LayoutInflater.inflate()时,系统会通过getClassLoader()获取这个自定义加载器来实例化插件中的自定义View类
类中重写了 getSystemService(),返回自定义的LayoutInflater,这个inflater绑定了插件的Context,确保资源解析的正确性
setOuterContext()将宿主Context设置为OuterContext,这样在插件中启动Activity、注册广播等操作时,系统会使用宿主环境来执行这些跨进程操作
上面操作确保插件中的类加载、资源访问和组件交互都能在正确的环境中执行
接下来将卡片布局加载到统一的容器中,在容器内加载布局启动activity等操作都使用的卡片context
![]() ![]()
package com.test.launcher.rear.card.appwidget
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.Display
import android.view.Gravity
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.view.children
class CardHostView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
private lateinit var contentView: View
private var decoratorView: View? = null
var cardInfo: CardInfo? = null
var initialLayout = 0
set(value) {
field = value
apply()
}
fun apply() {
contentView = getDefaultView()
removeAllViews()
contentView.setCorner(getDimen(baseDimen.baseapp_auto_dp_32).toFloat())
addView(contentView, LayoutParams(-1, -1))
}
fun getDefaultView(): View {
var defaultView: View? = null
try {
val layoutId: Int = initialLayout
defaultView = LayoutInflater.from(context).inflate(layoutId, this, false)
setOnClickListener {
defaultView?.callOnClick()
}
} catch (exception: RuntimeException) {
Logger.e("Error inflating AppWidget $cardInfo", exception)
}
if (defaultView == null) {
Logger.w("getDefaultView couldn't find any view, so inflating error")
defaultView = getErrorView()
}
return defaultView
}
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
return !(parentView()?.inEditeMode ?: false) && super.dispatchKeyEvent(event)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return !(parentView()?.inEditeMode ?: false) && super.dispatchTouchEvent(ev)
}
fun exitEditeMode() {
decoratorView?.let {
removeView(it)
}
}
private fun getErrorView(): View {
val tv = TextView(context)
tv.gravity = Gravity.CENTER
tv.setText(com.android.internal.R.string.gadget_host_error_inflating)
tv.setBackgroundColor(Color.argb(127, 0, 0, 0))
return tv
}
fun getContentView(): View {
return contentView
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
Logger.d("${contentView::class.java.name}#${contentView.hashCode()} onAttachedToWindow")
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
Logger.d("${contentView::class.java.name}#${contentView.hashCode()} onDetachedFromWindow")
}
fun View.parentView() = parent?.parent as? FocusLimitRecycleView
companion object {
fun obtain(context: Context, card: CardModel): CardHostView {
val packageName = card.info.componentName.packageName
val pluginContext =
if (packageName == context.packageName) context else
PluginContextWrapper.createFromPackage(packageName, context.display)
return CardHostView(pluginContext).also {
it.id = View.generateViewId()
it.isFocusable = false
it.cardInfo = card.info
it.initialLayout = when (card.info.sizeStyle) {
1 -> card.info.smallLayout
3 -> card.info.bigLayout
else -> card.info.mediumLayout
}
}
}
}
open fun updateChildState(it: Boolean, recyclerView: FocusLimitRecycleView) {
val inTouchMode = recyclerView.isInTouchMode
val hasFocus = recyclerView.hasFocus()
val parent = parent as? ViewGroup
Logger.d("parent isInTouchMode $inTouchMode $hasFocus")
if (it) {
if (hasFocus && !inTouchMode) {
if (recyclerView.getEditeChild() == parent?.tag) {
parent?.descendantFocusability = FOCUS_BLOCK_DESCENDANTS
getContentView().alpha = 1f
} else {
parent?.descendantFocusability = FOCUS_AFTER_DESCENDANTS
getContentView().alpha = 0.4f
}
}
} else {
getContentView().alpha = 1f
parent?.visible()
}
}
}
View Code
在launcher中直接 CardHostView.obtain(mBinding.root.context,it) 创建卡片显示在桌面
来源:https://www.cnblogs.com/LiuZhen/p/19174303 |