Android 管理桌面控件
2023-01-30 本文已影响0人
gaookey
image.png
桌面控件是通过 BroadcastReceiver
的形式来进行控制的,因此每个桌面控件都对应于一个 BroadcastReceiver
。 为了简化桌面控件的开发,Android 系统提供了一个 AppWidgetProvider
类,它就是 BroadcastReceiver
的子类。也就是说,开发者开发桌面控件只要缴承 AppWidgetProvider
类即可。
public class DesktopApp extends AppWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 加载指定界面布局文件,创建RemoteViews对象
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_widget); // ①
// 为show ImageView设置图片
remoteViews.setImageViewResource(R.id.show, R.drawable.image); // ②
// 将AppWidgetProvider的子类实例包装成ComponentName对象
ComponentName componentName = new ComponentName(context, DesktopApp.class); // ③
// 调用AppWidgetManager将remoteViews添加到ComponentName中
appWidgetManager.updateAppWidget(componentName, remoteViews); // ④
}
}
layout/my_widget.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/show"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:src="@drawable/logo" />
</LinearLayout>
xml/appwidget_provider.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 指定该桌面控件的基本配置信息:
minWidth:桌面控件的最小宽度。
minWidth:桌面控件的最小高度。
updatePeriodMillis:更新频率
initialLayout:初始时显示的布局 -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/my_widget"
android:minWidth="150dip"
android:minHeight="70dip"
android:updatePeriodMillis="1000" />
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication"
tools:targetApi="31">
<receiver
android:name=".DesktopApp"
android:exported="true"
android:label="@string/app_name"
tools:ignore="IntentFilterExportedReceiver">
<!-- 将该BroadcastReceiver当成桌面控件 -->
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<!-- 指定桌面控件的meta-data -->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_provider" />
</receiver>
</application>
</manifest>
把应用安装到 Android 系统上,然后桌面长按,选择 Widgets
拖动控件到桌面
image.png image.png实例:液晶时钟
LedClock
public class LedClock extends AppWidgetProvider {
private Timer timer = new Timer();
private AppWidgetManager appWidgetManager;
private Context context;
// 将0~9的液晶数字图片定义成数组
private int[] digits = new int[]{R.drawable.su01, R.drawable.su02,
R.drawable.su03, R.drawable.su04, R.drawable.su05,
R.drawable.su06, R.drawable.su07, R.drawable.su08,
R.drawable.su09, R.drawable.su10};
// 将显示小时、分钟、秒钟的ImageView定义成数组
private int[] digitViews = new int[]{R.id.img01, R.id.img02, R.id.img04,
R.id.img05, R.id.img07, R.id.img08};
static class MyHandler extends Handler {
private WeakReference<LedClock> ledClock;
public MyHandler(WeakReference<LedClock> ledClock) {
this.ledClock = ledClock;
}
@Override
public void handleMessage(Message msg) {
if (msg.what == 0x123) {
RemoteViews views = new RemoteViews(ledClock.get().context.getPackageName(), R.layout.clock);
// 定义SimpleDateFormat对象
SimpleDateFormat df = new SimpleDateFormat("HHmmss");
// 将当前时间格式化成HHmmss的形式
String timeStr = df.format(new Date());
for (int i = 0; i < timeStr.length(); i++) {
// 将第i个数字字符转换为对应的数字
int num = timeStr.charAt(i) - 48;
// 将第i个图片设为对应的液晶数字图片
views.setImageViewResource(ledClock.get().digitViews[i],
ledClock.get().digits[num]);
}
// 将AppWidgetProvider子类实例包装成ComponentName对象
ComponentName componentName = new ComponentName(ledClock.get().context,
LedClock.class);
// 调用AppWidgetManager将remoteViews添加到ComponentName中
ledClock.get().appWidgetManager.updateAppWidget(
componentName, views);
}
super.handleMessage(msg);
}
}
private Handler handler = new MyHandler(new WeakReference<>(this));
@Override
public void onUpdate(Context context,
AppWidgetManager appWidgetManager, int[] appWidgetIds) {
System.out.println("--onUpdate--");
this.appWidgetManager = appWidgetManager;
this.context = context;
// 定义计时器
timer = new Timer();
// 启动周期性调度
timer.schedule(new TimerTask() {
@Override
public void run() {
// 发送空消息,通知界面更新
handler.sendEmptyMessage(0x123);
}
}, 0, 1000);
}
}
layout/clock.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<!-- 定义8个ImageView来显示液晶数字 -->
<ImageView
android:id="@+id/img01"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/img02"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/img03"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/su00" />
<ImageView
android:id="@+id/img04"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/img05"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/img06"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/su00" />
<ImageView
android:id="@+id/img07"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/img08"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
xml/my_clock.xml
<?xml version="1.0" encoding="utf-8"?><!-- 指定该桌面组件的基本配置信息:
minWidth:桌面控件的最小宽度。
minWidth:桌面控件的最小高度。
updatePeriodMillis:更新频率
initialLayout:初始时显示的布局 -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/clock"
android:minWidth="200dp"
android:minHeight="20dp"
android:updatePeriodMillis="1000" />
AndroidManifest.xml
<receiver
android:name=".LedClock"
android:exported="true"
android:label="@string/name"
tools:ignore="IntentFilterExportedReceiver">
<!-- 将该BroadcastReceiver当成桌面控件 -->
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<!-- 指定桌面控件的meta-data -->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/my_clock" />
</receiver>
image.png
显示带数据集的桌面控件
StackWidgetService
public class StackWidgetService extends RemoteViewsService {
// 重写该方法,该方法返回一个RemoteViewsFactory对象
// RemoteViewsFactory对象的作用类似于Adapter
// 它负责为RemoteView中的指定组件提供多个列表项
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new StackRemoteViewsFactory(this.getApplicationContext(), intent); // ①
}
class StackRemoteViewsFactory implements RemoteViewsFactory {
private Context mContext;
private Intent intent;
StackRemoteViewsFactory(Context mContext, Intent intent) {
this.mContext = mContext;
this.intent = intent;
}
// 定义一个数组来保存该组件生成的多个列表项
private int[] items;
@Override
public void onCreate() {
// 初始化items数组
items = new int[]{R.drawable.bomb5, R.drawable.bomb6,
R.drawable.bomb7, R.drawable.bomb8, R.drawable.bomb9,
R.drawable.bomb10, R.drawable.bomb11, R.drawable.bomb12,
R.drawable.bomb13, R.drawable.bomb14, R.drawable.bomb15,
R.drawable.bomb16};
}
@Override
public void onDestroy() {
items = null;
}
// 该方法的返回值控制该对象包含多少个列表项
@Override
public int getCount() {
return items.length;
}
// 该方法的返回值控制各位置所显示的RemoteViews
@Override
public RemoteViews getViewAt(int position) {
// 创建RemoteViews对象,加载/res/layout目录下的widget_item.xml文件
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
// 更新widget_item.xml布局文件中的widget_item组件
rv.setImageViewResource(R.id.widget_item, items[position]);
// 创建Intent,用于传递数据
Intent fillInIntent = new Intent();
fillInIntent.putExtra(StackWidgetProvider.EXTRA_ITEM, position);
// 设置当单击该RemoteViews时传递fillInIntent包含的数据
rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);
// 此处让线程暂停0.2秒来模拟加载该组件
System.out.println("加载【" + position + "】位置的组件");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return rv;
}
@Override
public RemoteViews getLoadingView() {
return null;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public void onDataSetChanged() {
}
}
}
layout/widget_item.xml
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_item"
android:layout_width="120dp"
android:layout_height="120dp"
android:gravity="center" />
StackWidgetProvider
public class StackWidgetProvider extends AppWidgetProvider {
public static final String TOAST_ACTION = "org.crazyit.desktop.TOAST_ACTION";
public static final String EXTRA_ITEM = "org.crazyit.desktop.EXTRA_ITEM";
@Override
public void onUpdate(Context context,
AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 创建RemoteViews对象,加载/res/layout目录下的widget_layout.xml文件
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
Intent intent = new Intent(context, StackWidgetService.class);
// 使用intent更新rv中的stack_view组件(StackView)
rv.setRemoteAdapter(R.id.stack_view, intent); // ①
// 设置当StackWidgetService提供的列表项为空时,直接显示empty_view组件
rv.setEmptyView(R.id.stack_view, R.id.empty_view);
// 创建启动StackWidgetProvider组件(作为BroadcastReceiver)的Intent
Intent toastIntent = new Intent(context, StackWidgetProvider.class);
// 为该Intent设置Action属性
toastIntent.setAction(StackWidgetProvider.TOAST_ACTION);
// 将Intent包装成PendingIntent
PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context,
0, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// 将PendingIntent与stack_view进行关联
rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent);
// 使用AppWidgetManager通过RemoteViews更新AppWidgetProvider
appWidgetManager.updateAppWidget(new ComponentName(context,
StackWidgetProvider.class), rv); // ②
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
// 重写该方法,将该组件当成BroadcastReceiver使用
@Override
public void onReceive(Context context, Intent intent) {
if (TOAST_ACTION.equals(intent.getAction())) {
// 获取Intent中的数据
int viewIndex = intent.getIntExtra(EXTRA_ITEM, 0);
// 显示Toast提示
Toast.makeText(context, "点击第" + viewIndex + "个列表项",
Toast.LENGTH_SHORT).show();
}
super.onReceive(context, intent);
}
}
layout/widget_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp">
<StackView
android:id="@+id/stack_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:loopViews="true" />
<TextView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff0f"
android:gravity="center"
android:text="@string/no_item"
android:textColor="#ffffff"
android:textSize="20sp"
android:textStyle="bold" />
</FrameLayout>
AndroidManifest.xml
<!-- 配置AppWidgetProvider,即配置桌面控件 -->
<receiver android:name=".StackWidgetProvider"
android:exported="true">
<!-- 通过该intent-filter指定该Receiver作为桌面控件 -->
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<!-- 为桌面控件指定meta-data -->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/stackwidgetinfo" />
</receiver>
<!-- 配置RemoteViewsService
必须指定权限为android.permission.BIND_REMOTEVIEWS
-->
<service
android:name=".StackWidgetService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
image.gif
摘抄至《疯狂Android讲义(第4版)》