Android 管理桌面控件

image.png

桌面控件是通過 BroadcastReceiver 的形式來進行控制的壶冒,因此每個桌面控件都對應(yīng)于一個 BroadcastReceiver。 為了簡化桌面控件的開發(fā)咸作,Android 系統(tǒng)提供了一個 AppWidgetProvider 類记罚,它就是 BroadcastReceiver 的子類桐智。也就是說然磷,開發(fā)者開發(fā)桌面控件只要繳承 AppWidgetProvider 類即可姿搜。

public class DesktopApp extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // 加載指定界面布局文件舅柜,創(chuàng)建RemoteViews對象
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_widget);  // ①
        // 為show ImageView設(shè)置圖片
        remoteViews.setImageViewResource(R.id.show, R.drawable.image);   // ②
        // 將AppWidgetProvider的子類實例包裝成ComponentName對象
        ComponentName componentName = new ComponentName(context, DesktopApp.class);  // ③
        // 調(diào)用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>

把應(yīng)用安裝到 Android 系統(tǒng)上芳悲,然后桌面長按谅年,選擇 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的液晶數(shù)字圖片定義成數(shù)組
    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定義成數(shù)組
    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個數(shù)字字符轉(zhuǎn)換為對應(yīng)的數(shù)字
                    int num = timeStr.charAt(i) - 48;
                    // 將第i個圖片設(shè)為對應(yīng)的液晶數(shù)字圖片
                    views.setImageViewResource(ledClock.get().digitViews[i],
                            ledClock.get().digits[num]);
                }
                // 將AppWidgetProvider子類實例包裝成ComponentName對象
                ComponentName componentName = new ComponentName(ledClock.get().context,
                        LedClock.class);
                // 調(diào)用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();
        // 啟動周期性調(diào)度
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                // 發(fā)送空消息,通知界面更新
                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來顯示液晶數(shù)字 -->
    <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

顯示帶數(shù)據(jù)集的桌面控件

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;
        }

        // 定義一個數(shù)組來保存該組件生成的多個列表項
        private int[] items;

        @Override
        public void onCreate() {
            // 初始化items數(shù)組
            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) {
            // 創(chuàng)建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]);
            // 創(chuàng)建Intent笆凌,用于傳遞數(shù)據(jù)
            Intent fillInIntent = new Intent();
            fillInIntent.putExtra(StackWidgetProvider.EXTRA_ITEM, position);
            // 設(shè)置當單擊該RemoteViews時傳遞fillInIntent包含的數(shù)據(jù)
            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) {
        // 創(chuàng)建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);  // ①
        // 設(shè)置當StackWidgetService提供的列表項為空時跪妥,直接顯示empty_view組件
        rv.setEmptyView(R.id.stack_view, R.id.empty_view);
        // 創(chuàng)建啟動StackWidgetProvider組件(作為BroadcastReceiver)的Intent
        Intent toastIntent = new Intent(context, StackWidgetProvider.class);
        // 為該Intent設(shè)置Action屬性
        toastIntent.setAction(StackWidgetProvider.TOAST_ACTION);
        // 將Intent包裝成PendingIntent
        PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context,
                0, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        // 將PendingIntent與stack_view進行關(guān)聯(lián)
        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中的數(shù)據(jù)
            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
        必須指定權(quán)限為android.permission.BIND_REMOTEVIEWS
         -->
        <service
            android:name=".StackWidgetService"
            android:exported="false"
            android:permission="android.permission.BIND_REMOTEVIEWS" />
image.gif

摘抄至《瘋狂Android講義(第4版)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沉帮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子川背,更是在濱河造成了極大的恐慌缴允,老刑警劉巖锈候,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡甫男,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門婆跑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來郊供,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長沈堡,這世上最難降的妖魔是什么诞丽? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任懂衩,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己怨咪,他們只是感情好肄鸽,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著堪澎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪味滞。 梳的紋絲不亂的頭發(fā)上樱蛤,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音桃犬,去河邊找鬼刹悴。 笑死,一個胖子當著我的面吹牛攒暇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播子房,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼形用,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了证杭?” 一聲冷哼從身側(cè)響起田度,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎解愤,沒想到半個月后镇饺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡送讲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年奸笤,在試婚紗的時候發(fā)現(xiàn)自己被綠了惋啃。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡监右,死狀恐怖边灭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情健盒,我是刑警寧澤绒瘦,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站扣癣,受9級特大地震影響惰帽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜父虑,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一善茎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧频轿,春花似錦垂涯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至膳殷,卻和暖如春操骡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赚窃。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工册招, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人勒极。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓是掰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親辱匿。 傳聞我的和親對象是個殘疾皇子键痛,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容