前段时间写代码,无意间看到了RemoteViews这个类,觉得挺陌生的,于是上网查资料,发现是一个可以改变通知栏和桌面小工具的布局的控件,于是开始研究桌面小工具,虽然代码很简单,但是坑超级多,而且网上的资料不是很全,很多都没有兼容8.0和9.0,下面我附上亲测有效的代码和demo。
桌面小工具(线程Thread版)https://github.com/linqinen708/MyAppWidgetProvider
桌面小工具(服务Service版)https://github.com/linqinen708/MyAppWidgetProvider2
其中服务版用Kotlin写的,不过还提供了Java类,被注释了,网友可以自己根据需要选择使用
实现的效果是,点击小工具变色,再次点击停止变色
1.新建:网上有说直接在新建project的时候选择App Widget,但是我自己测试的时候发现直接无法运行app,因为系统没有检测到启动的activity,所以启动按钮无法点击,于是还是按照一般的project方式新建
2.写布局:随便建一个布局,名字要注意后面要用到,我命名widget_content.xml,这个布局是展示自己桌面上的内容的,其中不是所有控件都支持,但是常用的控件大部分都是支持的
FrameLayout
LinearLayout
RelativeLayout
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
其中要注意!!!,新的布局ConstraintLayout是不支持的,我是采坑了之后才知道的。。。。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv_content"
android:layout_width="230dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:text="桌面上的文字内容"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#ff6666"
/>
</RelativeLayout>
3.对桌面控件的大小和其它属性进行设置,这个特殊的xml文件要放在xml文件夹下,remote_views_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_content"
android:minWidth="260dp"
android:minHeight="60dp"
android:updatePeriodMillis="8640000">
<!--
initialLayout:桌面小组件的布局XML文件
minHeight:桌面小组件的最小显示高度
minWidth:桌面小组件的最小显示宽度
updatePeriodMillis:桌面小组件的更新周期。这个周期最短是30分钟
-->
</appwidget-provider>
4.下面讲重点!AppWidgetProvider
public class MyAppWidgetProvider extends AppWidgetProvider {
private static final String CLICK_NAME_ACTION = "com.action.widget.click";
private final int colors[] = {Color.BLUE, Color.DKGRAY, Color.GREEN, Color.RED, Color.CYAN,
Color.WHITE, Color.GRAY, Color.MAGENTA, Color.LTGRAY, Color.YELLOW};
private static RemoteViews remoteViews;
private static ComponentName componentName;
private TimerThread mTimerThread;
public TimerThread getTimerThread(Context context) {
if (mTimerThread == null) {
mTimerThread = new TimerThread(context);
}
return mTimerThread;
}
public RemoteViews getRemoteViews(Context context) {
if (remoteViews == null) {
LogT.i("创建RemoteViews");
remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_content);
}
return remoteViews;
}
public ComponentName getComponentName(Context context) {
if (componentName == null) {
LogT.i("创建ComponentName");
componentName = new ComponentName(context, MyAppWidgetProvider.class);
}
return componentName;
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
LogT.i("刷新AppWidgetProvider");
setOnClickEvent(appWidgetManager, context);
}
@Override
public void onEnabled(Context context) {
super.onEnabled(context);
LogT.i("启动WidgetProvider");
}
@Override
public void onDisabled(Context context) {
super.onDisabled(context);
LogT.i("销毁WidgetProvider");
}
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
LogT.i("接收广播");
if (intent != null && CLICK_NAME_ACTION.equals(intent.getAction())) {
Toast.makeText(context, "触发点击事件", Toast.LENGTH_LONG).show();
if (TimerThread.isContinue) {
TimerThread.isContinue = false;
} else {
TimerThread.isContinue = true;
getTimerThread(context).start();
}
setOnClickEvent(AppWidgetManager.getInstance(context), context);
} else if (intent != null && TimerThread.ACTION_TIMER.equals(intent.getAction())) {
int index = TimerThread.index % colors.length;
// LogT.i("index:" + index);
getRemoteViews(context).setTextColor(R.id.tv_content, colors[index]);
AppWidgetManager appWidgetManger = AppWidgetManager.getInstance(context);
/*只有触发刷新方法updateAppWidget()才能有效果*/
appWidgetManger.updateAppWidget(getComponentName(context), getRemoteViews(context));
}
// setOnClickEvent(appWidgetManger,context);
}
private void setOnClickEvent(AppWidgetManager appWidgeManger, Context context) {
if (componentName == null || remoteViews == null) {
LogT.i("刷新点击事件" );
Intent intentClick = new Intent(CLICK_NAME_ACTION);
/*如果没有下面这句话,这点击事件无效*/
intentClick.setComponent(getComponentName(context));
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
intentClick, PendingIntent.FLAG_UPDATE_CURRENT);
getRemoteViews(context).setOnClickPendingIntent(R.id.tv_content, pendingIntent);
appWidgeManger.updateAppWidget(getComponentName(context), getRemoteViews(context));
}
}
}
想要实现桌面的各种功能需要新建一个类,继承AppWidgetProvider,如果你点击进入源代码,发现这是一个广播,也就是说,桌面上的各种功能实际上是通过广播的形式实现的。
这里有一个坑!!!就是每次收到广播,AppWidgetProvider都会重新创建,具体原因不明,是在打日志的时候发现的,怀疑是AppWidgetProvider为了某种功能或者性能才这样做的,这样一来,小伙伴要注意是否要设置单例模式或者static,来储存对象了
AppWidgetProvider有好几个方法onUpdate、onEnabled、onDisabled,看名字就知道什么功能,重点是onUpdate这个方法,就是刷新使用,如果有类似于点击事件这些功能,建议放在这里刷新,否则有可能出现点击事件无效的情况
其中有2个非常重要的对象RemoteViews和ComponentName
RemoteViews是用来导入你的桌面布局的,也就是上面第二步提到的widget_content,
这个有两个参数,第一个是包名,第二个就是布局,RemoteViews(String packageName, int layoutId)
ComponentName这个对象是来锁定你的AppWidgetProvider,只要你想改变布局,比如文字,颜色等等都会需要,并触发updateAppWidget方法
其中有两个重载方法,
第一个是控件的id,只改变局部某个控件
updateAppWidget(int appWidgetId, RemoteViews views)
第二个方法是ComponentName 对象,改变所有控件,建议用这个,原因下面会提到
updateAppWidget(ComponentName provider, RemoteViews views)
我将RemoteViews和ComponentName都设置成了static,这样可以节省性能,否则每次都要创建,感觉怪怪的,当我使用updateAppWidget(ComponentName provider, RemoteViews views)来更新界面时,就不需要考虑任何控件了,反正都刷新了,毕竟小工具的布局都很简单
appWidgeManger.updateAppWidget(getComponentName(context), getRemoteViews(context));
5.点击事件
桌面小工具的点击事件很特殊,没有直接setOnClick的方法,不过有setOnClickPendingIntent方法
Intent intentClick = new Intent(CLICK_NAME_ACTION);
/*如果没有下面这句话,这点击事件无效*/
intentClick.setComponent(getComponentName(context));
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
intentClick, PendingIntent.FLAG_UPDATE_CURRENT);
getRemoteViews(context).setOnClickPendingIntent(R.id.tv_content, pendingIntent);
appWidgeManger.updateAppWidget(getComponentName(context), getRemoteViews(context));
6.广播
既然AppWidgetProvider继承BroadcastReceiver,就一定会有onReceive方法,看源码你会发现,什么onUpdate、onEnabled、onDisabled等方法都是通过广播的方式触发的,其中我们的点击事件,也是通过广播的形式触发的,既然是广播,肯定要注册
<receiver android:name=".broadcastreceiver.MyAppWidgetProvider">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/remote_views_layout"/>
<intent-filter>
<action android:name="com.action.widget.click"/>
<action android:name="com.action.widget.timer"/>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
</receiver>
其中remote_views_layout就是第3步提到的设置桌面布局大小的。
android:name="android.appwidget.provider"和
android:name=“android.appwidget.action.APPWIDGET_UPDATE”
这两个不要改动,用来判断你的app是否支持桌面小工具用的,其中APPWIDGET_UPDATE就是系统自带的广播,用来刷新桌面小工具布局的
com.action.widget.click 桌面点击事件的广播
com.action.widget.timer 计时器广播(如果使用service就可以不需要,下面会提到)
7.线程thread
因为要实现变颜色的效果,所以我使用了线程,如果功能复杂的话,小伙伴们也可以使用service(这里有坑,后面会提到)
public class TimerThread extends Thread {
public static String ACTION_TIMER = "com.action.widget.timer";
// public static final String ACTION_TIMER = "android.appwidget.action.APPWIDGET_UPDATE";
public static boolean isContinue = false;
private Context mContext;
public static int index = 0;
/**
* android8.0之后只能发送显式广播了,
* 如果还想发送隐式广播,可以用系统自带的一些广播
* android.appwidget.action.APPWIDGET_UPDATE
* 是AppWidgetProvider类原本自带的广播,用来更新控件
* 如果小伙伴们有需要可以使用
*/
public TimerThread(Context context) {
mContext = context;
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// LogT.i("8.0系统,使用系统自带的广播");
// ACTION_TIMER = "android.appwidget.action.APPWIDGET_UPDATE";
// }
}
@Override
public synchronized void start() {
super.start();
LogT.i("启动线程");
}
@Override
public void interrupt() {
super.interrupt();
LogT.i("打断线程");
}
@Override
public void run() {
// super.run();
while (isContinue) {
Intent intent = new Intent(ACTION_TIMER);
try {
/*原本是每秒改变一次,但是实际使用中,细心的小伙伴会发现是大于1s的
* 因为程序在运行代码的时候也是消耗时间的,小伙伴们可以根据自己的需求进行优化
* */
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (mContext == null) {
isContinue = false;
return;
}
intent.setClass(mContext, MyAppWidgetProvider.class);
mContext.sendBroadcast(intent);
index++;
// LogT.i("11index:" + index);
/*以下代码小伙伴自己根据需求改动,我是设置了180秒后停止
* 不设置的话理论上来说无限循环,真的是理论上,没有亲测多久停止
* */
if (index > 180) {
isContinue = false;
index = 0;
}
}
}
}
注意!!!8.0以上不允许使用静态广播,所以要加入类别
intent.setClass(mContext, MyAppWidgetProvider.class);
8.补充说明:如果小伙伴用到Service来实现桌面小工具的一些功能,要注意:
1.Android8.0以上版本无法直接启动Serviece,而是startForegroundService,你们需要额外判断,
2.在9.0以上还需要在注册表加入
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
参考资料:
https://www.cnblogs.com/itgungnir/p/6923949.html 这个是网友写的时间小工具,不过没有兼容8.0以上版本,但是例子还是很经典的