Android自己定制的桌面小组件

2,707 阅读6分钟
  • 在使用手机的过程中,我们会发现桌面上有各种各样的小组件。比如显示步数,显示天气的。或者刷码支付的。这个时候我就在思考,能否定义一个属于自已的手机的桌面的小组件的呢?毕竟手机自己的小组件要么是功能单一,要么是需要在应用商店付费使用。
  • 下面的这个就是自已定义的视图。由于我觉得下面这些数据对于自己来说,是比较关注的。所以就有了这样一个想法。Screenshot_20240819_093202.png
  • 下面我们来分析代码的具体实现。
  • 在mi_step包下新建Step类
package com.fly.babynurse.mi_step;

import androidx.annotation.NonNull;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Step {
    public int getmId() {
        return mId;
    }

    public long getmBeginTime() {
        return mBeginTime;
    }

    public long getmEndTime() {
        return mEndTime;
    }

    public int getmMode() {
        return mMode;
    }

    public int getmSteps() {
        return mSteps;
    }

    private int mId; // 记录在sqlite的id
    private long mBeginTime; // 计步开始时间
    private long mEndTime; // 计步结束时间
    private int mMode; // 计步模式: 0:不支持模式, 1:静止, 2:走路, 3:跑步, 11:骑车, 12:交通工具
    private int mSteps; // 总步数

    public Step(int id, long beginTime, long endTime, int mode, int steps)
    {
        mId = id;
        mBeginTime = beginTime;
        mEndTime = endTime;
        mMode = mode;
        mSteps = steps;
    }

    public String getTime(long time){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
        String currentTime = sdf.format(time);
        return currentTime;
    }

    @Override
    public String toString() {
        return "Step{" +
                "mId=" + mId +
                ", mBeginTime=" + getTime(mBeginTime) +
                ", mEndTime=" + getTime(mEndTime) +
                ", mMode=" + mMode +
                ", mSteps=" + mSteps +
                '}';
    }
}
  • 然后再封装一个stepManager类
package com.fly.babynurse.mi_step;

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;

import java.lang.reflect.Method;
import java.text.DecimalFormat;
import java.util.Calendar;
import java.util.LinkedList;

public class StepManager {
    public static final double ava_dis = 0.592d ;//平均步长0.592m

    public static final double ava_cal = 74.24d ;//,每行走一公里大约消耗74.24千卡的能量

    public static class FeatureParser {//FeatureParser,通过反射机制来获取miui.util.FeatureParser
        public static boolean getBoolean(String name, boolean defaultValue) {
            try {
                Class featureParserClass = Class.forName("miui.util.FeatureParser");
                Method method = featureParserClass.getMethod("getBoolean", String.class, boolean.class);
                return (Boolean) method.invoke(null, name, defaultValue);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return defaultValue;
        }
    }


    //判断是否支持step的功能
    public static boolean isSupport(){
        return FeatureParser.getBoolean("support_steps_provider",false);
    }


    //在功能开始之前判断是否支持stepsProvider功能
    public static String[] projection = new String[] {
            Steps.ID,
            Steps.BEGIN_TIME,
            Steps.END_TIME,
            Steps.MODE,
            Steps.STEPS
    };
    public static LinkedList<Step> getAllSteps(Context context,String selection, String[] args) {
        ContentResolver resolver = context.getContentResolver();
        LinkedList<Step> steps = new LinkedList<Step>();
        Cursor cursor = resolver.query(Steps.CONTENT_URI, projection, selection, args,
                Steps.DEFAULT_SORT_ORDER);
        if (cursor.moveToFirst()) {
            do {
                Step s = new Step(cursor.getInt(0), cursor.getLong(1), cursor.getLong(2),
                        cursor.getInt(3),
                        cursor.getInt(4));
                steps.add(s);
            } while (cursor.moveToNext());
        }
        return steps;
    }


    //获取今天零点的时间戳
    private static long getTodayBeginTime() {
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        return calendar.getTimeInMillis();
    }

    /**
     * 获取今日的step
     * @param context
     */
    public static LinkedList<Step> getToDaySteps(Context context) {
        return getAllSteps(context, Steps.BEGIN_TIME + ">?",
                new String[] { String.valueOf(getTodayBeginTime()) });
    }

    /**
     * 获取今日的step
     * @param context
     * @return
     */
    public static long getTodayStepsCount(Context context){
        long totalSteps= 0;
        if(StepManager.isSupport()){
            LinkedList<Step> steps= StepManager.getToDaySteps(context);
            for (Step step:steps){
                Log.d("step",step.toString());
                totalSteps+=step.getmSteps();
            }
            Log.e("step", "totalSteps="+totalSteps);

        }
        return totalSteps;
    }


    /**
     * 获取今日的距离
     * @param context
     * @return
     */
    public static String  getTodayDisValue(Context context){
       return String.format("%.2f",ava_dis*getTodayStepsCount(context)/1000);
    }

    public static String getTodayCalValue(Context context){
        return String.format("%.2f",ava_cal*Double.parseDouble(getTodayDisValue(context)));
    }
}
  • 编写steps
package com.fly.babynurse.mi_step;

import android.net.Uri;

public class Steps {
    /* Data Field */
    public static final String ID = "_id";
    public static final String BEGIN_TIME = "_begin_time";
    public static final String END_TIME = "_end_time";
    // 0: NOT SUPPORT 1:REST 2:WALKING 3:RUNNING
    public static final String MODE = "_mode";
    public static final String STEPS = "_steps";
    /* Default sort order */
    public static final String DEFAULT_SORT_ORDER = "_id asc";
    /* Authority */
    public static final String AUTHORITY = "com.miui.providers.steps";
    /* Content URI */
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/item");
}

编写GasPriceWidgetProvider,继承于** AppWidgertProvider**,这个类主要实现桌面小组件的视图的显示和数据的获取和刷新。通过getGasPriceApi 来获取油价,通过getTime来获取数据更新的时间,通过getTodaySteps来获取今日步数,通过getTodayCal类获取今日燃烧的卡路里,通过getBattery来获取电池的电量,通过getTodayDis来获取今日行走的距离。

package com.fly.babynurse.gasWidget;

import static android.content.Context.BATTERY_SERVICE;
import static com.fly.babynurse.mi_step.StepManager.getTodayCalValue;
import static com.fly.babynurse.mi_step.StepManager.getTodayStepsCount;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.BatteryManager;
import android.os.Build;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;

import com.fly.babynurse.MainActivity;
import com.fly.babynurse.R;
import com.fly.babynurse.gasWidget.res.GasBeanRes;
import com.fly.babynurse.mi_step.Step;
import com.fly.babynurse.mi_step.StepManager;
import com.google.gson.Gson;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

// AppWidgetProvider实现
public class GasPriceWidgetProvider extends AppWidgetProvider {


    public static final String ACTION_GAS = "ACTION_GAS";
    public static final String ACTION_BATTERY = "ACTION_BATTERY";

    public static final String ACTION_TIME = "ACTION_TIME";

    public static final String ACTION_STEP = "ACTION_STEP";

    public static final String ACTION_DIS = "ACTION_DIS";

    public static final String ACTION_CAL = "ACTION_CAL";


    private static final String TAG = "GasPriceWidgetProvider";

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // 更新所有Widget的数据
        // 更新今日油价
        Log.e(TAG, "onUpdate:");
        getData(context);


    }

    private void setClick(Context context) {

    }

    private void getData(Context context) {
        getGasPriceApi(context);//使用api获取油价
        getBattery(context);//获取今日电量
        getTime(context);//获取更新的时间
        getTodaySteps(context);//获取今日的step
        getToDayDis(context);//获取已走距离
        getTodayCal(context);
    }




    //处理点击事件
    private PendingIntent getSelfPendingIntent(Context context, String action) {
        Intent intent = new Intent(context, GasPriceWidgetProvider.class);
        intent.setAction(action);
        return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
    }

    Intent widgetIntent;

    @Override
    public void onEnabled(Context context) {
        super.onEnabled(context);
    }


    @Override
    public void onDisabled(Context context) {

    }

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        String action = intent.getAction();
        Log.e(TAG, "onReceive: " + action);
        switch (Objects.requireNonNull(action)) {
            case ACTION_GAS:
                  getGasPriceApi(context);
                break;
            case ACTION_BATTERY:
                getBattery(context);
                break;
            case ACTION_TIME:
                getTime(context);
                break;
            case ACTION_STEP:
                getTodaySteps(context);
                break;
            case ACTION_DIS:
                getToDayDis(context);
                break;
            case ACTION_CAL:
                 getTodayCal(context);
                break;
            case "android.appwidget.action.APPWIDGET_UPDATE":
                break;
            case "android.appwidget.action.APPWIDGET_DISABLED":
                break;
            case "android.appwidget.action.APPWIDGET_ENABLED":
                break;
            case "android.appwidget.action.APPWIDGET_OPTIONS_CHANGED":
                break;
            case "android.appwidget.action.APPWIDGET_UPDATE_OPTIONS":
                break;
            default:
                break;
        }
    }

    /**
     * 获取今日的行走距离
     * @param context
     */
    private void getToDayDis(Context context) {
        String todayDis =StepManager.getTodayDisValue(context);
        updateWidget(context, R.id.text_dis, "已走距离: "+ todayDis + "km", ACTION_DIS);
    }




    private void updateWidget(Context context, int res, String content, String action) {
        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
        ComponentName thisWidget = new ComponentName(context, GasPriceWidgetProvider.class);
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
        views.setTextViewText(res, content);
        views.setOnClickPendingIntent(res, getSelfPendingIntent(context, action));
        appWidgetManager.updateAppWidget(thisWidget, views);
    }


    public static final String gasUrl = "https://api.oioweb.cn/api/common/GasolinePriceQuery";

    //获取今日油价信息
    private void getGasPriceApi(Context context) {//使用api获取油价
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(gasUrl)
                .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(@NonNull Call call, IOException e) {
                // 请求失败的处理逻辑
                Log.e(TAG, "onFailure: " + e.getMessage());
            }

            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
                dealRes(context, call, response);
            }
        });

    }

    //处理油价信息
    private void dealRes(Context context, @NonNull Call call, @NonNull Response response) throws IOException {
        if (response.isSuccessful()) {
            assert response.body() != null;
            String responseBody = response.body().string();
            GasBeanRes gasPriceBean = new Gson().fromJson(responseBody, GasBeanRes.class);
            for (GasBeanRes.ResultBean resultBean : gasPriceBean.getResult()) {
                if (resultBean.getRegion().equals("湖北")) {
                    // 更新Widget布局
                    Log.e(TAG, "resultBean.getGasoline92() = " + resultBean.getGasoline92());
                    updateWidget(context, R.id.text_gas_price, "92油价: " + resultBean.getGasoline92() + "/L", ACTION_GAS);
                    break;
                }
            }
            // 处理获取到的油价信息
        } else {
            // 处理错误响应
            Log.e(TAG, "onFailure: " + response.message());
        }
    }

    //获取电池信息
    private void getBattery(Context context) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            BatteryManager batteryManager = (BatteryManager) context.getSystemService(BATTERY_SERVICE);
            int batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
            updateWidget(context, R.id.text_battery, "今日电量: " + batteryLevel + "%", ACTION_BATTERY);
        }
    }

    private void getTime(Context context){
        Calendar calendar = Calendar.getInstance();
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        String currentTime = sdf.format(calendar.getTime());
        updateWidget(context,R.id.text_time,currentTime,ACTION_TIME);
    }

    //获取今日的step
    private void getTodaySteps(Context context){
            updateWidget(context,R.id.text_step,"今日步数: " +getTodayStepsCount(context),ACTION_STEP);
        }

    private void getTodayCal(Context context) {
        updateWidget(context,R.id.text_cal,"消耗cal: " +getTodayCalValue(context)+"千卡",ACTION_CAL);
    }

}

其中onReceive方法处理不同的广播动作以触发特定的行为,在updateWidget里面,通过传入不同的action意图,来让广播接受器接收不同的指令来执行对应的数据的请求和视图渲染和更新的行为。其中onUpdate,该方法会在Wiget更新时被调用。通过getData方法负责获取和更新 Widget 显示的数据。这包括从网络获取数据、获取系统信息等,并更新 Widget 的显示内容。

  • 在AndroidMainifest里面增加相关代码
<receiver android:exported="true" android:name=".gasWidget.GasPriceWidgetProvider">
<intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    <action android:name="ACTION_GAS"/>
    <action android:name="ACTION_BATTERY"/>
    <action android:name="ACTION_TIME"/>
    <action android:name="ACTION_STEP"/>
    <action android:name="ACTION_DIS"/>
    <action android:name="ACTION_CAL"/>
</intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/my_widget_info" />
</receiver>

其中@xml/my_widget_info 执行的是这个小组件的基本信息

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="294dp"
    android:minHeight="144dp"
    android:updatePeriodMillis="1800000"
    android:initialLayout="@layout/widget_layout"
    android:widgetCategory="home_screen"
    >
    </appwidget-provider>

其中android:updatePeriodMillis="1800000"代表小组件更新的周期为30分钟,这里需要注意的是,小组件数据更新的最小周期为30分钟,即使你设置小于30分终,它也会按照30分钟的间隔来更新数据。

  • 优化建议,假如需要每15分钟来更新数据,可以使用WorkManager类更新数据
 public class GasPriceUpdateWorker extends Worker {
       @NonNull
       @Override
       public Result doWork() {
           Context context = getApplicationContext();
           GasPriceWidgetProvider provider = new GasPriceWidgetProvider();
           provider.getData(context);
           return Result.success();
       }
   }

在onEnable中调度Worker:

 @Override
   public void onDisabled(Context context) {
       super.onDisabled(context);
       WorkManager.getInstance(context)
               .cancelUniqueWork("GasPriceUpdate");
   }

这样,我们就可以使用自已的桌面小组件了。如果你有好的想法和创意,那么你也可以是实现。代码如花花,代码如诗。