• Android 桌面小组件使用


    本文为作者原创,允许转载,不过请在文章开头明显处注明链接和出处!!! 谢谢配合~
    作者:stars-one
    链接:https://www.cnblogs.com/stars-one/p/18073082

    本篇大约有8950个字,阅读预计需要11.19分钟


    原文: Android 桌面小组件使用-Stars-One的杂货小窝

    借助公司上的几个项目,算是学习了Android桌面小组件的用法,记下踩坑记录

    基本步骤

    1.创建小组件布局

    这里需要注意的事,小组件布局里不能使用自定义View,只能使用原生的组件,比如说LinearLayout,TextView,连约束布局都不能使用

    
    <LinearLayout 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="wrap_content"
        android:background="@color/white"
        android:orientation="vertical"
        android:padding="16dp">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
    
            <TextView
                android:id="@+id/tvDate"
                style="@style/textStyle14"
                android:textColor="#313131"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="2023-12-10" />
    
            <ImageView
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1" />
    
            <TextView
                android:id="@+id/tvTime"
                android:textColor="#313131"
                style="@style/textStyle14"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="12:10" />
    
        LinearLayout>
    
        <LinearLayout
            android:layout_marginTop="16dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/result_clean"/>
    
            <LinearLayout
                android:orientation="vertical"
                android:layout_width="0dp"
                android:layout_marginStart="9dp"
                android:gravity="center_vertical"
                android:layout_height="match_parent"
                android:layout_weight="1" >
                <TextView
                    style="@style/textStyle14"
                    android:textColor="#313131"
                    android:textStyle="bold"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="125.4MB"/>
                <TextView
                    style="@style/textStyle14"
                    android:textColor="#313131"
                    android:textStyle="bold"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Junk"/>
            LinearLayout>
    
            <TextView
                android:layout_gravity="center_vertical"
                android:id="@+id/tvClean"
                android:textColor="#313131"
                style="@style/textStyle14"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Clean" />
    
        LinearLayout>
    
    LinearLayout>
    

    2.创建provider

    import android.appwidget.AppWidgetManager
    import android.appwidget.AppWidgetProvider
    import android.content.Context
    import android.widget.RemoteViews
    import android.widget.RemoteViews.RemoteView
    import ten.jou.recover.R
    
    class CleaningWidget : AppWidgetProvider() {
        override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray
        ) {
            appWidgetIds.forEach {
    			//如果小组件布局中使用不支持的组件,这里创建RemoteViews时候,IDE会报红提示!
                val remoteView = RemoteViews(context.packageName, R.layout.widget_layout)
    			//绑定数据
    			remoteView.setTextViewText(R.id.tv1,"hello world")
                appWidgetManager.updateAppWidget(it, remoteView)
            }
    
        }
    }
    

    AppWidgetProvider本质就是一个广播接收器,所以在清单文件需要声明(见步骤4)

    这里先补充下,RemoteViews对于TextView,ImageView等View,有设置文本,字体颜色,图片等相关方法,但并不是所有方法都支持,绑定数据的时候需要注意下小组件是否支持!

    3.创建xml属性声明

    在xml文件夹里新建widget_info.xml文件:

    
    <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:targetCellWidth="4"
        android:targetCellHeight="2"
        android:minWidth="250dp"
        android:minHeight="110dp"
        android:updatePeriodMillis="0"
        android:initialLayout="@layout/widget_layout"
        tools:targetApi="s">
    appwidget-provider>
    

    Android12版本以上新增的2个属性,声明组件是4*2大小

    • targetCellWidth
    • targetCellHeight

    4.清单文件声明

    <receiver
    	android:name=".view.CleaningWidget"
    	android:enabled="true"
    	android:exported="true">
    	<intent-filter>
    		<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    	intent-filter>
    	<meta-data
    		android:name="android.appwidget.provider"
    		android:resource="@xml/widget_info" />
    receiver>
    

    5.代码添加小组件

    官方说Android12不允许直接通过代码添加小组件,只能让用户手动去桌面拖动添加,但是我手头的三星系统却是支持的(也是Android12),具体还没有细究...

    而官方文档上的写的例子如下:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    	val context = this@DesktopWidgetActivity
    
    	val appWidgetManager: AppWidgetManager =
    		context.getSystemService(AppWidgetManager::class.java)
    	val myProvider = ComponentName(context, CleaningWidget::class.java)
    
    	//判断启动器是否支持小组件pin
    	val successCallback = if (appWidgetManager.isRequestPinAppWidgetSupported) {
    		// Create the PendingIntent object only if your app needs to be notified
    		// that the user allowed the widget to be pinned. Note that, if the pinning
    		// operation fails, your app isn't notified.
    		Intent(context, CleaningWidget::class.java).let { intent ->
    			// Configure the intent so that your app's broadcast receiver gets
    			// the callback successfully. This callback receives the ID of the
    			// newly-pinned widget (EXTRA_APPWIDGET_ID).
    			//适配android12的
    			val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    				PendingIntent.FLAG_MUTABLE
    			} else {
    				PendingIntent.FLAG_UPDATE_CURRENT
    			}
    			PendingIntent.getBroadcast(
    				context,
    				0,
    				intent,
    				flags
    			)
    		}
    	} else {
    		null
    	}
    
    	appWidgetManager.requestPinAppWidget(myProvider, null, successCallback)
    }
    

    这里提下,上面的设置flags方法

    val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    	PendingIntent.FLAG_MUTABLE
    } else {
    	PendingIntent.FLAG_UPDATE_CURRENT
    }
    

    有个新项目的targetSdk为34(即Android14),如果使用上面的代码会出现下面崩溃错误提示

    Targeting U+ (version 34 and above) disallows creating or retrieving a PendingIntent with FLAG_MUTABLE, an implicit Intent within and without FLAG_NO_CREATE and FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT for security reasons. To retrieve an already existing PendingIntent, use FLAG_NO_CREATE, however, to create a new PendingIntent with an implicit Intent use FLAG_IMMUTABLE.
    

    实际上提示已经告诉我们怎么去改代码了,我这里把PendingIntent.FLAG_MUTABLE改为FLAG_IMMUTABLE就不会出现了上述的崩溃问题

    应该是Android14添加的限制:

    • 如果Intent不传数据,必须使用PendingIntent.FLAG_IMMUTABLE
    • 如果是需要传递数据,则还是需要使用PendingIntent.FLAG_MUTABLE

    定时刷新小组件UI

    首先,我们得知道,如何主动去更新数据:

    val context = it.context
    val appWidgetManager: AppWidgetManager = context.getSystemService(AppWidgetManager::class.java)
    val myProvider = ComponentName(context, CleaningWidget::class.java)
    val remoview = CleaningWidget.getRemoteViewTest(context)
    
    //更新某类组件
    appWidgetManager.updateAppWidget(myProvider,remoview)
    //更新具体某个组件id
    appWidgetManager.updateAppWidget(widgetId,remoview)
    

    getRemoteViewTest方法就是创建一个remoteview,然后调用remoteview相关方法设置文本之类的进行数据填充,代码就略过不写了,详见上述基本步骤2

    上面的方法我们注意到updateAppWidget可以传不同的参数,一般我们用的第二个方法,指定更新某个组件

    但这里又是需要我们传一个组件id,所以就是在步骤2的时候,我们根据需要需要存储下widgetId比较好,一般存入数据库,或者使用SharePreference也可

    然后,就是对于定时的情况和对应方案:

    1. 如果是间隔多长更新一次,可以使用开一个服务,在服务中开启协程进行
    2. 如果是单纯的时间文本更新,可以使用TextClock组件,比如说 12:21这种
    3. 小组件的xml中默认可以设置定时更新时长,不过最短只能需要15分钟
    4. 可以使用闹钟服务AlarmManager来实现定时,不过此用法需要结合pendingintent和广播接收器使用,最终要在广播接收器里调用更新数据方法
    5. JobScheduler来实现定时更新,似乎受系统省电策略影响,适用于不太精确的定时事件(官方文档上推荐这个)
    6. WorkManager来实现定时更新(实际上算是JobScheduler升级版),似乎受系统省电策略影响,适用于不太精确的定时事件

    应该是除了第一种方法,其他都是可以在应用被杀死的情况进行更新小组件UI

    小组件播放动画

    progressbar实现

    帧动画不手动调用anim.start()方法是不会播放的,然后在网上看到一篇文章,使用了progressbar来实现,步骤如下:

    在drawable文件夹准备帧动画文件

    
    <animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false" android:visible="true">
        <item android:drawable="@drawable/cat_1" android:duration="100" />
        <item android:drawable="@drawable/cat_2" android:duration="100" />
        <item android:drawable="@drawable/cat_3" android:duration="100" />
        <item android:drawable="@drawable/cat_4" android:duration="100" />
    animation-list>
    
    <ProgressBar
    	android:indeterminateDrawable="@drawable/cat_animdrawable"
    	android:layout_width="wrap_content"
    	android:layout_height="wrap_content"/>
    

    indeterminateDrawable设置为上面的帧动画文件即可

    layoutanim实现

    主要是利用viewgroup的初次显示的时候,会展示当前view的添加动画效果,从而实现比较简单的动画效果,如平移,缩放等

    可以看实现的敲木鱼一文Android-桌面小组件RemoteViews播放木鱼动画 - 掘金

    使用ViewFlipper

    ViewFlipper主要是轮播使用的

    里面可放几个元素,之后通过设置autoStart为true,则保证自动轮播

    flipInterval属性则是每个元素的间隔时间(帧动画的时间),单位为ms

    不过在remoteview中使用的话,缺点就是里面的元素数目只能固定死

    否则只能通过定义不同layout文件(如3个元素则是某个layout,4个元素则是某个layout,然后根据选择来创建remoteview)

    <ViewFlipper
            android:id="@+id/viewFlipper"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:layout_margin="4dp"
            android:autoStart="true"
            android:flipInterval="800">
    
            <ImageView
                android:id="@+id/vf_img_1"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="fitXY"
                android:src="@drawable/peace_talisman_1" />
    
            <ImageView
                android:id="@+id/vf_img_2"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="fitXY"
                android:src="@drawable/peace_talisman_2" />
        ViewFlipper>
    

    补充

    获取当前桌面的组件id列表

    //获得当前桌面已添加的组件的id列表(可能用户添加了多个)
    val context = it.context
    val appWidgetManager: AppWidgetManager = context.getSystemService(AppWidgetManager::class.java)
    val myProvider = ComponentName(context, CleaningWidget::class.java)
    
    val info =appWidgetManager.getAppWidgetIds(myProvider)
    toast(info.size.toString())
    

    参考

  • 相关阅读:
    (八)、基于 LangChain 实现大模型应用程序开发 | 基于知识库的个性化问答 (检索 Retrieval)
    6.jQuery中的Ajax上传文件
    大厂频繁联手,NFT 与 GameFi 的融合能带来哪些新叙事?
    自定义线程实现c++代码回调run方法
    交换两数整有几种途径
    websocket技术详解,附带springboot实例
    heic批量转jpg文件
    C# 32应用程序获取64位操作系统注册表
    零信任沙盒,加密沙盒,防泄密沙盒
    Towards End-to-End Unified Scene Text Detection and Layout Analysis(2022)
  • 原文地址:https://www.cnblogs.com/stars-one/p/18073082