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 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版)》

上一篇下一篇

猜你喜欢

热点阅读