這篇我要來聊聊關於Android開發中一個很有趣的環節: 桌面小工具〔´∇`〕
桌面小工具我不知道算是常見需求還是不算常見的需求XDDD
我自己是沒遇過QQ
不過最近我上Matters看到一篇文章我覺得非常有趣
這篇文章的作者他抓了他Likecoin錢包的餘額,將其顯示在主畫面上供自己隨時查看(IOS)
所以仿照它的功能邏輯,我也做了一個Likecoin於手機畫面顯示的Android版
那關於這個東西我目前做出來的感覺是這樣
系統每幾秒鐘就會掃描一次你最新的Likecoin數量,並且也可以透過手動的方式更新
而這種能長久保存狀態的方式就是使用Service完成
關於Service的部分我之前有寫過一款音樂播放器作為範例演示使用Service,各位可以去看看
->碼農日常-『Android studio』應用Service完成一個簡單的音樂播放器
那這次...我當然不會直接用Likecoin作為範例演示XD
因為其中還牽扯網路通訊okHttp3跟其他一些有的沒的,整體寫下來會有點混亂,而且Likecoin本身雖然是公開資料但卻不是公開資料云云....
反正問題太多了,我不會介紹的
取而代之,我就另外再寫了一個桌面時間小工具,作為本次的範例
然後Github在此
->https://github.com/thumbb13555/AppWidgetDemo
好,開始吧
1. 概念
桌面小工具,其對應之Android元件為AppWidgetProvider
根據官網介紹,本元件繼承自BroadcastReceiver
在本站,我也有寫過關於Broadcast使用方法,大家可以參考參考
碼農日常-『Android studio』Broadcast廣播的應用方法
那他既然會繼承該方法,表示他所有的傳輸都是倚靠著Broadcast再操作其行為的
官網同時也在下面介紹了AppWidgetProvider的生命週期回調
如下
/**接收廣播*/ @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); } /**當廣播接到ACTION_APPWIDGET_UPDATE調用*/ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); } /**當小工具被刪除時調用,每刪一個就調用一次*/ @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); } /**當小工具被添加到桌面時調用,只會被調用一次*/ @Override public void onEnabled(Context context) { super.onEnabled(context); } /**當小工具自桌面刪除到最後一個時調用,只會被調用一次*/ @Override public void onDisabled(Context context) { super.onDisabled(context); } /**當程序從備份中調用時執行*/ @Override public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) { super.onRestored(context, oldWidgetIds, newWidgetIds); } /**當小工具有被變更尺寸時回調此一方法*/ @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); }
基本上應該寫得很清楚每個回調都在幹什麼了,接下來就是實際來撰寫我們的小工具(・ω・)b
喔差點忘了,本次的專案結構長這樣,敬請參考
2. 建立一個AppWidget
呃...其實Android studio簡直貼心到不行,原本都要自己東設定西設定的東西,他直接幫你把模板寫好了(攤手)
我當時找資料時,我找到的第一篇是這篇
->https://blog.csdn.net/singwhatiwanna/article/details/16961661
對,然後,我就用了最土炮的方法來寫Widget了XD
後來再進一步查詢其他資料後才發現其實Android studio 早就幫你寫好模板了,建立一下不出幾秒鐘.....
(囧)
那麼,我們就來建立一番吧
首先請在專案資料夾上按右鍵->New->Widget->AppWidget
然後填入以下資訊
其中的Minimum Width跟Minimum Height是最小長寬,編輯器會檔自幫你轉為格子數量,還蠻貼心的XD
當然,這日後都是可以改的那個稍後再講
那等他跑一下...就會出現如下內容了
然後在AndroidManifest.xml內則會出現如下
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.noahliu.appwidgetdemo"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.AppWidgetDemo"> <receiver android:name=".MyTimeWidget"> <intent-filter > <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/my_time_widget_info" /> </receiver> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
這時候可以按下運行,就可以看到桌面小工具囉XD
但是還不夠,我要如何寫介面呢?
OK,很簡單;請至layout的my_time_widget_info.xml中修改為以下樣式
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/darker_gray" android:padding="@dimen/widget_margin" android:orientation="vertical" > <Button android:id="@+id/button_Hello" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> <TextView android:id="@+id/textView_TimeLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:layout_margin="8dp" android:gravity="center" android:text="Time" android:textColor="@android:color/white" android:textSize="24sp" android:textStyle="bold" /> </LinearLayout>
注意,目前AppWidget不支援使用androidx的套件,所以無法使用Constraintlayout喔!要注意
那麼上面說,想改元件尺大小怎麽辦?
答案是至xml的
my_time_widget_info.xml內去修改囉
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialKeyguardLayout="@layout/my_time_widget" android:initialLayout="@layout/my_time_widget" android:minWidth="220dp" android:minHeight="80dp" android:previewImage="@drawable/ic_baseline_access_time_24" android:resizeMode="horizontal|vertical" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen"></appwidget-provider>
其中,改icon可以從粉底白字的位置修改
而大小則是從橘底白字的位置修改喔
3. 撰寫Service
接下來要撰寫Service的部分了,我的要求就是這個Service要在背景一直執行我所要求的行為
那關於掛Service的方法,我之前就有講過,便不再贅述,直接上程式吧
TimeService.java(此處僅呈現時間功能)
public class TimeService extends Service implements Runnable { public static final String TAG = MyTimeWidget.TAG; public static final String CLICK_EVENT = "android.appwidget.action.Click"; @SuppressLint("SimpleDateFormat") private SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd, HH:mm:ss"); @SuppressLint("HandlerLeak") private Handler handler = new Handler(){ @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); if (msg.what == 1){ update(); } } }; @Override public void run() { handler.sendEmptyMessage(1); handler.postDelayed(this,1000); } @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate:(Service) "); handler.sendEmptyMessage(1); handler.post(this); } /**更新時間*/ private void update(){ String time = sdf.format(new Date()); RemoteViews views = new RemoteViews(getPackageName(),R.layout.my_time_widget); views.setTextViewText(R.id.textView_TimeLabel,time); AppWidgetManager manager = AppWidgetManager.getInstance(getApplicationContext()); ComponentName componentName = new ComponentName(getApplicationContext(),MyTimeWidget.class); manager.updateAppWidget(componentName,views); } }
首先,Handler跟run的執行緒執行我在這篇有講過,不知道的可以去看看
->碼農日常-『Android studio』Android背景執行之Thread、Handler及AsyncTask的基礎用法
然後,讓我們Focus重點,就是在update()的內容
/**更新時間*/ private void update(){ String time = sdf.format(new Date()); RemoteViews views = new RemoteViews(getPackageName(),R.layout.my_time_widget); views.setTextViewText(R.id.textView_TimeLabel,time); AppWidgetManager manager = AppWidgetManager.getInstance(getApplicationContext()); ComponentName componentName = new ComponentName(getApplicationContext(),MyTimeWidget.class); manager.updateAppWidget(componentName,views); }
此處就是更新元件的部分,其實比較好的做法是在這邊發起廣播去通知AppWidgetProvider的inReceive去做更新的....不過我反倒覺得這樣也是挺方便的XD
那首先,就是RemoteViews這個類別,這算是比較沒這麼熟悉的類別
不過看名字也知,這算是可以遠程操作View的一種類別
也是我們主要於修改元件本體的操作
其他的部分就是連結上AppWidget的部分,以及更新所有的Widget
4. 點擊事件
再來是點擊事件,一樣PO全文畫重點
public class TimeService extends Service implements Runnable { public static final String TAG = MyTimeWidget.TAG; public static final String CLICK_EVENT = "android.appwidget.action.Click"; @SuppressLint("SimpleDateFormat") private SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd, HH:mm:ss"); @SuppressLint("HandlerLeak") private Handler handler = new Handler(){ @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); if (msg.what == 1){ update(); } } }; @Override public void run() { handler.sendEmptyMessage(1); handler.postDelayed(this,1000); } @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate:(Service) "); handler.sendEmptyMessage(1); handler.post(this); } @Override public void onStart(Intent intent, int startId) { super.onStart(intent, startId); Log.d(TAG, "onStart:(Service) "); if (intent.getAction() != null){ if (intent.getAction().equals(CLICK_EVENT)){ Handler toastHandler = new Handler(Looper.getMainLooper()); toastHandler.post(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "點擊了小物件", Toast.LENGTH_SHORT).show(); } }); } } setButtonClick(); } /**設置按鈕廣播發送事件*/ private void setButtonClick() { ComponentName thisWidget = new ComponentName(this,MyTimeWidget.class); AppWidgetManager manager = AppWidgetManager.getInstance(this); RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.my_time_widget); Intent myIntent = new Intent(); myIntent.setAction(CLICK_EVENT); PendingIntent pendingIntent = PendingIntent.getService(this,0,myIntent,0); remoteViews.setOnClickPendingIntent(R.id.button_Hello,pendingIntent); manager.updateAppWidget(thisWidget,remoteViews); } /**更新時間*/ private void update(){ String time = sdf.format(new Date()); RemoteViews views = new RemoteViews(getPackageName(),R.layout.my_time_widget); views.setTextViewText(R.id.textView_TimeLabel,time); AppWidgetManager manager = AppWidgetManager.getInstance(getApplicationContext()); ComponentName componentName = new ComponentName(getApplicationContext(),MyTimeWidget.class); manager.updateAppWidget(componentName,views); } }
先先注意到設定按鈕的部分
private void setButtonClick() { ComponentName thisWidget = new ComponentName(this,MyTimeWidget.class); AppWidgetManager manager = AppWidgetManager.getInstance(this); RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.my_time_widget); Intent myIntent = new Intent(); myIntent.setAction(CLICK_EVENT); PendingIntent pendingIntent = PendingIntent.getService(this,0,myIntent,0); remoteViews.setOnClickPendingIntent(R.id.button_Hello,pendingIntent); manager.updateAppWidget(thisWidget,remoteViews); }
設定按鈕點擊事件,是使用PendingIntent作為發送intent
然後我們再看到onStart的部分
@Override public void onStart(Intent intent, int startId) { super.onStart(intent, startId); Log.d(TAG, "onStart:(Service) "); if (intent.getAction() != null){ if (intent.getAction().equals(CLICK_EVENT)){ Handler toastHandler = new Handler(Looper.getMainLooper()); toastHandler.post(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "點擊了小物件", Toast.LENGTH_SHORT).show(); } }); } } setButtonClick(); }
而當每次點擊按鈕時,intent就會送一個廣播出來
然後會在onStart內接收到廣播,並且在intent內顯示內容
不過,onStart作為生命週期的一環,一開始進來時會是null,所以記得要做null例外處理喔!
到此完成點擊Service要做的工作後,我們就來啟動Service了!
5.啟用Service
啟用Service的部分,我們由MyTimeWidget.java這邊去開啟
看~全~文~~~
public class MyTimeWidget extends AppWidgetProvider { public static final String TAG = "BalanceServiceMy"; /**接收廣播資訊*/ @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); Log.d(TAG, "onReceive: "+intent.getAction()); switch (intent.getAction()){ case "android.appwidget.action.APPWIDGET_UPDATE": Boolean isRun = isServiceRun(context); Log.d(TAG, "onReceive: 有Service再跑?: "+isRun); if (!isRun)startRunService(context); break; } } /**當小工具被建立時*/ @Override public void onEnabled(Context context) { startRunService(context); } /**當小工具被刪除時*/ @Override public void onDisabled(Context context) { context.stopService(new Intent(context,TimeService.class)); } /**啟動Service*/ private void startRunService(Context context) { Intent intent = new Intent(context,TimeService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent); } context.startService(intent); } /**判斷此是否已有我的Service再跑*/ private Boolean isServiceRun(Context context){ ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningServiceInfo> list = manager.getRunningServices(Integer.MAX_VALUE); for (ActivityManager.RunningServiceInfo info : list){ if (TimeService.class.getName().equals(info.service.getClassName()))return true; } return false; } }
這裡有標示幾種顏色,首先來看看紫底白字
這部分是開啟Service,Service的部分再Android Oreo之後必須還得加入context.startForegroundService(intent);,否則線程會很快被背景殺死..
再來,為了使Service操作正常,在開啟Service前我會先檢查有沒有相同Service再跑,有則略過,也就是黃底白字部分
最後,是停掉Service(粉底白字);我將他寫在onDisabled內,表示當所有小工具都被刪掉時,即停止運作Service
ok,應該沒有其他要注意的了,在這邊按下執行就可以跑囉:D
留言列表