<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      方案一、aar架包集成

      最簡單直接的方案,卡片側實現(xiàn),打成aar包提供到launcher顯示

      方案二、AppWidget

      原生的桌面小組件方案,被限制無法自定義view

      底層通過BroadcastReceiver實現(xiàn)

      方案三、插件方案

      插件方案有好幾種,實現(xiàn)原理都是通過配置實現(xiàn),其中有Service,BroadcastReceiver,Plugin

      在SystemUI模塊中,狀態(tài)欄等模塊很多使用的都是Plugin方案跟Service方案

      這里詳細講通過Service配置跟Plugin配置實現(xiàn)

      插件方案可以實現(xiàn)卡片跟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,統(tǒng)一接口

      然后在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 中,用于后續(xù)卡片內加載布局

      通過PathClassLoader構建的類加載器包含了插件APK的路徑,當調用LayoutInflater.inflate()時,系統(tǒng)會通過getClassLoader()獲取這個自定義加載器來實例化插件中的自定義View類

      類中重寫了 getSystemService(),返回自定義的LayoutInflater,這個inflater綁定了插件的Context,確保資源解析的正確性

      setOuterContext()將宿主Context設置為OuterContext,這樣在插件中啟動Activity、注冊廣播等操作時,系統(tǒng)會使用宿主環(huán)境來執(zhí)行這些跨進程操作

      上面操作確保插件中的類加載、資源訪問和組件交互都能在正確的環(huán)境中執(zhí)行

      接下來將卡片布局加載到統(tǒng)一的容器中,在容器內加載布局啟動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) 創(chuàng)建卡片顯示在桌面 

      posted on 2025-10-30 14:55  翻滾的咸魚  閱讀(15)  評論(0)    收藏  舉報

      主站蜘蛛池模板: 宿州市| 国产口爆吞精在线视频2020版| 亚洲av肉欲一区二区| 无码国产一区二区三区四区| 日本成人午夜一区二区三区| 国产成人无码免费视频在线| XXXXXHD亚洲日本HD| 中文国产日韩欧美二视频| 18禁在线一区二区三区| 国产做a爱片久久毛片a片| 欧美精品在线观看| 激情综合色综合久久综合| h无码精品3d动漫在线观看| 内地偷拍一区二区三区| 人妻聚色窝窝人体WWW一区| 在线观看国产成人AV天堂| 99精品国产一区二区三| 亚洲青青草视频在线播放| 久久人人爽爽人人爽人人片av| 亚洲 中文 欧美 日韩 在线| 国产午夜亚洲精品福利| 亚洲一区二区精品偷拍| 精品午夜福利无人区乱码| 老司机亚洲精品一区二区| 国产极品粉嫩尤物一线天| 亚洲一区二区偷拍精品| 正安县| 美女一区二区三区亚洲麻豆| 另类 专区 欧美 制服| 2021国产成人精品久久| 国产短视频精品一区二区| 亚洲成aⅴ人在线观看| 乱人伦人妻中文字幕不卡| 日本久久精品一区二区三区| 精品少妇人妻av无码久久| 亚洲综合伊人久久大杳蕉| 另类 专区 欧美 制服| 久热这里有精品视频播放| 色综合AV综合无码综合网站| 亚洲av片在线免费观看| 欧美激情在线播放|