找回密码
 立即注册
首页 业界区 安全 Launcher 卡片框架多模块集成

Launcher 卡片框架多模块集成

茅香馨 5 天前
方案一、aar架包集成
最简单直接的方案,卡片侧实现,打成aar包提供到launcher显示
方案二、AppWidget
原生的桌面小组件方案,被限制无法自定义view
底层通过BroadcastReceiver实现
方案三、插件方案

插件方案有好几种,实现原理都是通过配置实现,其中有Service,BroadcastReceiver,Plugin
在SystemUI模块中,状态栏等模块很多使用的都是Plugin方案跟Service方案
这里详细讲通过Service配置跟Plugin配置实现
插件方案可以实现卡片跟launcher解耦,并且可以自定义view,还支持跨进程交互
首先定义一个插件,用于配置卡片信息,exported 属性标识可以给其它应用读取
  1.                                                                                                                                              
复制代码
View Code
  1. package com.example.page  import android.content.Context  interface Plugin {      fun onCreate(hostContext: Context, pluginContext: Context) {     }      fun onDestroy() {     } }  class PagerWidgetPlugin : Plugin
复制代码
Plugin
  1. 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 编写卡片布局
  1.                            
复制代码
cards_remote_control_layout
复制代码
pager_control_layout 然后在launcher中,使用 AppWidgetManager 来读取配置信息
  1. 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 也可以通过加载器获取
  1. 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
  1. 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
  1. 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
  1. 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  
  1. 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) 创建卡片显示在桌面 

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册