方案一、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) 创建卡片显示在桌面  
 
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除 
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |