方案一、aar架包集成
最简单直接的方案,卡片侧实现,打成aar包提供到launcher显示
方案二、AppWidget
原生的桌面小组件方案,被限制无法自定义view
底层通过BroadcastReceiver实现
方案三、插件方案
插件方案有好几种,实现原理都是通过配置实现,其中有Service,BroadcastReceiver,Plugin
在SystemUI模块中,状态栏等模块很多使用的都是Plugin方案跟Service方案
这里详细讲通过Service配置跟Plugin配置实现
插件方案可以实现卡片跟launcher解耦,并且可以自定义view,还支持跨进程交互
首先定义一个插件,用于配置卡片信息,exported 属性标识可以给其它应用读取
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
  pager_widget_info   remote_control_widget_info 编写卡片布局
  cards_remote_control_layout   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() private var mAppWidgetChangeListener: ((MutableList) -> Unit)? = null val showOnCards = UnPeekLiveData(mutableListOf()) 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) -> 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() 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() val libPaths = mutableListOf() 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 ) : 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) 创建卡片显示在桌面
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |