若是本文帮助到你,本人不胜荣幸,若是浪费了你的时间,本人深感抱歉。 但愿用最简单的大白话来帮助那些像我同样的人。若是有什么错误,请必定指出,以避免误导你们、也误导我。 本文来自:www.jianshu.com/u/320f9e8f7… 感谢您的关注。android
Android 桌面小部件是咱们常常看到的,好比时钟、天气、音乐播放器等等。 它可让 App 的某些功能直接展现在桌面上,极大的增长了用户的关注度。git
首先纠正一个误区: 当 App 的小部件被放到了桌面以后,并不表明你的 App 就能够一直在手机后台运行了。该被杀,它仍是会被杀掉的。 因此若是你作小部件的目的是为了让程序常驻后台,那么你能够死心了。github
可是!!! 虽然它仍是能被杀掉,可是用户能看的见它了啊,用户能够点击就打开咱们的 APP,因此仍是很不错的。网络
小部件能够作什么呢?也就是咱们须要实现什么功能。app
这三个功能,大概就能知足咱们绝大部分需求了吧。ide
若是你历来没有作过桌面部件,那确定老是感受有点慌,无从下手,毫无逻辑。 因此,实现它到底须要什么呢?工具
- 先声明 Widget 的一些属性。 在 res 新建 xml 文件夹,建立 appwidget-provider 标签的 xml 文件。
- 建立桌面要显示的布局。 在 layout 建立 app_widget.xml。
- 而后来管理 Widget 状态。 实现一个继承 AppWidgetProvider 的类。
- 最后在 AndroidManifest.xml 里,将 AppWidgetProvider类 和 xml属性 注册到一块。
- 一般咱们会加一个 Service 来控制 Widget 的更新时间,后面再讲为何。
作完这些,若是不出错,就完成了桌面部件。 其实挺简单的,下面就让咱们来看看具体的实现吧。布局
先上效果图:spa
在 res 新建 xml 文件夹,建立一个 app_widget.xml 的文件。 若是 res 下没有 xml 文件,则先建立。线程
app_widget.xml 内容以下:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/app_widget"
android:minHeight="110dp"
android:minWidth="110dp"
android:previewImage="@mipmap/ic_launcher"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen|keyguard">
<!--
android:minWidth : 最小宽度
android:minHeight : 最小高度
android:updatePeriodMillis : 更新widget的时间间隔(ms),"86400000"为1个小时,值小于30分钟时,会被设置为30分钟。能够用 service、AlarmManager、Timer 控制。
android:previewImage : 预览图片,拖动小部件到桌面时有个预览图
android:initialLayout : 加载到桌面时对应的布局文件
android:resizeMode : 拉伸的方向。horizontal表示能够水平拉伸,vertical表示能够竖直拉伸
android:widgetCategory : 被显示的位置。home_screen:将widget添加到桌面,keyguard:widget能够被添加到锁屏界面。
android:initialKeyguardLayout : 加载到锁屏界面时对应的布局文件
-->
</appwidget-provider>
复制代码
属性的注释在上面写的很清楚了,这里须要说两点。
在 layout 建立 app_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:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/widget_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="36sp"
android:textStyle="bold"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/widget_btn_reset"
style="@style/Widget.AppCompat.Toolbar.Button.Navigation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="恢复"/>
<Button
android:id="@+id/widget_btn_open"
style="@style/Widget.AppCompat.Toolbar.Button.Navigation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="打开页面"/>
</LinearLayout>
</LinearLayout>
复制代码
这里要注意的就是 桌面部件并不支持 Android 全部的控件。 支持的控件以下:
App Widget支持的布局:
FrameLayout
LinearLayout
RelativeLayout
GridLayout
App Widget支持的控件:
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper
复制代码
这里代码看起来可能有点多,先听我讲几个逻辑,再来看代码。
同一个小部件是能够添加屡次的,因此更新控件的时候,要把全部的都更新。
onReceive() 用来接收广播,它并不在生命周期里。可是,其实 onReceive() 是掌控生命周期的。 以下是 onReceive() 父类的源码,右边是每一个广播对应的方法。 上面我画的生命周期的图,也比较清楚。
而后咱们再来看代码。 新建一个 WidgetProvider 类,继承 AppWidgetProvider。 主要逻辑在 onReceive() 里,其余的都是生命周期切换时,所处理的事情。 咱们在下面分析 onReceive()。
public class WidgetProvider extends AppWidgetProvider {
// 更新 widget 的广播对应的action
private final String ACTION_UPDATE_ALL = "com.lyl.widget.UPDATE_ALL";
// 保存 widget 的id的HashSet,每新建一个 widget 都会为该 widget 分配一个 id。
private static Set idsSet = new HashSet();
public static int mIndex;
/**
* 接收窗口小部件点击时发送的广播
*/
@Override
public void onReceive(final Context context, Intent intent) {
super.onReceive(context, intent);
final String action = intent.getAction();
if (ACTION_UPDATE_ALL.equals(action)) {
// “更新”广播
updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);
} else if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
// “按钮点击”广播
mIndex = 0;
updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);
}
}
// 更新全部的 widget
private void updateAllAppWidgets(Context context, AppWidgetManager appWidgetManager, Set set) {
// widget 的id
int appID;
// 迭代器,用于遍历全部保存的widget的id
Iterator it = set.iterator();
// 要显示的那个数字,每更新一次 + 1
mIndex++; // TODO:能够在这里作更多的逻辑操做,好比:数据处理、网络请求等。而后去显示数据
while (it.hasNext()) {
appID = ((Integer) it.next()).intValue();
// 获取 example_appwidget.xml 对应的RemoteViews
RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.app_widget);
// 设置显示数字
remoteView.setTextViewText(R.id.widget_txt, String.valueOf(mIndex));
// 设置点击按钮对应的PendingIntent:即点击按钮时,发送广播。
remoteView.setOnClickPendingIntent(R.id.widget_btn_reset, getResetPendingIntent(context));
remoteView.setOnClickPendingIntent(R.id.widget_btn_open, getOpenPendingIntent(context));
// 更新 widget
appWidgetManager.updateAppWidget(appID, remoteView);
}
}
/**
* 获取 重置数字的广播
*/
private PendingIntent getResetPendingIntent(Context context) {
Intent intent = new Intent();
intent.setClass(context, WidgetProvider.class);
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0);
return pi;
}
/**
* 获取 打开 MainActivity 的 PendingIntent
*/
private PendingIntent getOpenPendingIntent(Context context) {
Intent intent = new Intent();
intent.setClass(context, MainActivity.class);
intent.putExtra("main", "这句话是我从桌面点开传过去的。");
PendingIntent pi = PendingIntent.getActivity(context, 0, intent, 0);
return pi;
}
/**
* 当该窗口小部件第一次添加到桌面时调用该方法,可添加屡次但只第一次调用
*/
@Override
public void onEnabled(Context context) {
// 在第一个 widget 被建立时,开启服务
Intent intent = new Intent(context, WidgetService.class);
context.startService(intent);
Toast.makeText(context, "开始计数", Toast.LENGTH_SHORT).show();
super.onEnabled(context);
}
// 当 widget 被初次添加 或者 当 widget 的大小被改变时,被调用
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle
newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
}
/**
* 当小部件从备份恢复时调用该方法
*/
@Override
public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
super.onRestored(context, oldWidgetIds, newWidgetIds);
}
/**
* 每次窗口小部件被点击更新都调用一次该方法
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
// 每次 widget 被建立时,对应的将widget的id添加到set中
for (int appWidgetId : appWidgetIds) {
idsSet.add(Integer.valueOf(appWidgetId));
}
}
/**
* 每删除一次窗口小部件就调用一次
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
// 当 widget 被删除时,对应的删除set中保存的widget的id
for (int appWidgetId : appWidgetIds) {
idsSet.remove(Integer.valueOf(appWidgetId));
}
super.onDeleted(context, appWidgetIds);
}
/**
* 当最后一个该窗口小部件删除时调用该方法,注意是最后一个
*/
@Override
public void onDisabled(Context context) {
// 在最后一个 widget 被删除时,终止服务
Intent intent = new Intent(context, WidgetService.class);
context.stopService(intent);
super.onDisabled(context);
}
}
复制代码
它传了两个值回来,Context 是跳转、发广播用的。 咱们用来判断的是 Intent ,这里用到了 Intent 的两种方式。
Intent 做为信息传递者。 它要把信息传给谁,能够有三个匹配依据:一个是action,一个是category,一个是data。
String ACTION_UPDATE_ALL = "com.lyl.widget.UPDATE_ALL"; 这个最后会在 AndroidManifest.xml 里面注册时写进去。 当每隔 N 秒/分钟,就发送一次这个广播,更新全部UI。
intent.hasCategory(Intent.CATEGORY_ALTERNATIVE) 是广播事件里携带的 Intent 里设置的,用来匹配。 点击“恢复”按钮,计数器清零。
而后是 updateAllAppWidgets() 这个方法,更新 UI。 更新 UI 用到了一个新东西——RemoteViews。
怎么来理解 RemoteViews 呢?
由于,桌面部件并不像日常布局直接展现,它须要经过某种服务去更新UI。可是咱们的App怎么能去控制桌面上的布局呢?
因此就须要有一个中间人,相似传递者。
我告诉传递者,你让他把个人 R.id.widget_txt ,更新成 “hello world”。 你让他把个人 R.id.widget_btn_open 按钮点击以后去响应 PendingIntent 这件事。
RemoteViews 就是承担着一个这样的角色。
而后再去理解代码,是否是稍微好一点了?
说好的 当每隔 N 秒/分钟,就发送一次这个广播。 那到底在哪发呢?也就是咱们刚开始说的,用 Service 来控制时间。
新建一个 WidgetService 类,继承 Service。代码以下:
/**
* 控制 桌面小部件 更新
* Created by lyl on 2017/8/23.
*/
public class WidgetService extends Service {
// 更新 widget 的广播对应的 action
private final String ACTION_UPDATE_ALL = "com.lyl.widget.UPDATE_ALL";
// 周期性更新 widget 的周期
private static final int UPDATE_TIME = 1000;
private Timer mTimer;
private TimerTask mTimerTask;
@Override
public void onCreate() {
super.onCreate();
// 每通过指定时间,发送一次广播
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
Intent updateIntent = new Intent(ACTION_UPDATE_ALL);
sendBroadcast(updateIntent);
}
};
mTimer.schedule(mTimerTask, 1000, UPDATE_TIME);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
mTimerTask.cancel();
mTimer.cancel();
}
/*
* 服务开始时,即调用startService()时,onStartCommand()被执行。
*
* 这个整形能够有四个返回值:start_sticky、start_no_sticky、START_REDELIVER_INTENT、START_STICKY_COMPATIBILITY。
* 它们的含义分别是:
* 1):START_STICKY:若是service进程被kill掉,保留service的状态为开始状态,但不保留递送的intent对象。随后系统会尝试从新建立service,
* 因为服务状态为开始状态,因此建立服务后必定会调用onStartCommand(Intent,int,int)方法。若是在此期间没有任何启动命令被传递到service,那么参数Intent将为null;
* 2):START_NOT_STICKY:“非粘性的”。使用这个返回值时,若是在执行完onStartCommand后,服务被异常kill掉,系统不会自动重启该服务;
* 3):START_REDELIVER_INTENT:重传Intent。使用这个返回值时,若是在执行完onStartCommand后,服务被异常kill掉,系统会自动重启该服务,并将Intent的值传入;
* 4):START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保证服务被kill后必定能重启。
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
return START_STICKY;
}
}
复制代码
在 onCreate 开启一个计时线程,每1秒发送一个广播,广播就是咱们本身定义的类型。
而后就只剩最后一步了,注册相关信息
<!-- 声明widget对应的AppWidgetProvider -->
<receiver android:name=".WidgetProvider">
<intent-filter>
<!--这个是必需要有的系统规定-->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<!--这个是咱们自定义的 action ,用来更新UI,还能够自由添加更多 -->
<action android:name="com.lyl.widget.UPDATE_ALL"/>
</intent-filter>
<!--要显示的布局-->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/app_widget"/>
</receiver>
<!-- 用来计时,发送 通知桌面部件更新 -->
<service android:name=".WidgetService" >
<intent-filter>
<!--用来启动服务-->
<action android:name="android.appwidget.action.APP_WIDGET_SERVICE" />
</intent-filter>
</service>
复制代码
相应的注释都在上面,若是咱们的App进程被杀掉,服务也被关掉,那就没办法更新UI了。 也能够再建立一个 BroadcastReceiver 监听系统的各类动态,来唤醒咱们的通知服务,这就属于进程保活了。
至此,以上代码写完,若是不出问题,运行以后直接去桌面看小工具,咱们的App就在里面了,能够添加到桌面。
对于须要定时更新的桌面部件,保证本身的服务在后台运行也是一件比较重要的事情。 这个咱们仍是能够好好作一下,毕竟用户都已经愿意把咱们的程序放到桌面上,因此只要友好的引导用户给你必定的权限,存活几率仍是很大。 再不济,让用户主动点开App,也不失为一种办法。
好的创意才能造就好的App,代码只是实现。
最后放上项目地址: github.com/Wing-Li/Wid…