桌面小工具以及AppWidgetProvider和RemoteViews的简单应用(兼容8.0和9.0)

前段时间写代码,无意间看到了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以上版本,但是例子还是很经典的

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
你可以通过以下步骤在Android上动态添加桌面小组件: 1. 创建小组件布局:首先,创建一个布局文件来定义小组件的外观。这可以在res/layout目录下的XML文件中完成。你可以使用不同的视图和布局来设计小组件。 2. 创建小组件提供者:创建一个扩展AppWidgetProvider类的Java类。这个类将负责处理小组件的生命周期事件,例如创建、更新和删除小组件。 3. 配置小组件提供者:在AndroidManifest.xml文件中,注册你的小组件提供者类。使用<receiver>标签来指定小组件提供者类,并使用<intent-filter>标签来定义处理小组件生命周期事件的操作。 4. 更新桌面:当用户将小组件拖放到桌面时,你需要在运行时动态添加小组件。为此,你可以使用AppWidgetManager类的updateAppWidget()方法来更新小组件视图。 下面是一个示例代码,展示如何动态添加桌面小组件: ```java // 创建小组件布局 RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout); // 配置小组件点击事件 Intent intent = new Intent(context, MyWidgetProvider.class); intent.setAction("ACTION_WIDGET_CLICKED"); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0); remoteViews.setOnClickPendingIntent(R.id.widget_button, pendingIntent); // 更新桌面小组件 AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); ComponentName componentName = new ComponentName(context, MyWidgetProvider.class); appWidgetManager.updateAppWidget(componentName, remoteViews); ``` 请注意,你需要根据你的需求适应以上代码,并根据你的小组件布局和逻辑进行修改。 希望这能帮助到你!如有任何进一步的问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值