這篇我要來聊聊關於Android開發中一個很有趣的環節: 桌面小工具〔´∇`〕

桌面小工具我不知道算是常見需求還是不算常見的需求XDDD

我自己是沒遇過QQ

不過最近我上Matters看到一篇文章我覺得非常有趣

->我终于把想做的东西做出来了。

這篇文章的作者他抓了他Likecoin錢包的餘額,將其顯示在主畫面上供自己隨時查看(IOS)

所以仿照它的功能邏輯,我也做了一個Likecoin於手機畫面顯示的Android版

那關於這個東西我目前做出來的感覺是這樣

Screenshot_1629013926

 

系統每幾秒鐘就會掃描一次你最新的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廣播的應用方法

截圖 2021-08-15 下午4.48.19

 

那他既然會繼承該方法,表示他所有的傳輸都是倚靠著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

喔差點忘了,本次的專案結構長這樣,敬請參考

截圖 2021-08-15 下午5.16.14

 

 


 

2. 建立一個AppWidget

 

呃...其實Android studio簡直貼心到不行,原本都要自己東設定西設定的東西,他直接幫你把模板寫好了(攤手)

我當時找資料時,我找到的第一篇是這篇

->https://blog.csdn.net/singwhatiwanna/article/details/16961661

對,然後,我就用了最土炮的方法來寫Widget了XD

後來再進一步查詢其他資料後才發現其實Android studio 早就幫你寫好模板了,建立一下不出幾秒鐘.....

(囧)

那麼,我們就來建立一番吧

首先請在專案資料夾上按右鍵->New->Widget->AppWidget

截圖 2021-08-14 下午7.15.53

 

然後填入以下資訊

截圖 2021-08-14 下午7.17.01

其中的Minimum Width跟Minimum Height是最小長寬,編輯器會檔自幫你轉為格子數量,還蠻貼心的XD

當然,這日後都是可以改的那個稍後再講

那等他跑一下...就會出現如下內容了

截圖 2021-08-15 下午5.28.25

然後在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中修改為以下樣式

截圖 2021-08-15 下午5.37.14

my_time_widget_info.xml

截圖 2021-08-15 下午5.37.50

<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的

截圖 2021-08-15 下午5.42.13

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的方法,我之前就有講過,便不再贅述,直接上程式吧

 

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全文畫重點

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

    @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這邊去開啟

 

看~全~文~~~

MyTimeWidget

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

 

TK

arrow
arrow

    碼農日常 發表在 痞客邦 留言(10) 人氣()