2

Android 桌面小组件使用 - Stars-one

 6 months ago
source link: https://www.cnblogs.com/stars-one/p/18073082
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Android 桌面小组件使用

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

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

1.创建小组件布局

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

<?xml version="1.0" encoding="utf-8"?>
<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文件:

<?xml version="1.0" encoding="utf-8"?>
<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文件夹准备帧动画文件

<?xml version="1.0" encoding="utf-8"?>
<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())

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK