您的位置:首页 > 移动开发

Android App Widget 开发

2017-09-07 13:43 351 查看

概述

App Widget是应用程序窗口小部件(Widget),是微型的应用程序视图,它可以被嵌入到其它应用程序中(比如桌面)并接收周期性的更新。你可以通过一个App Widget Provider来发布一个Widget。

Widget布局

appwidget-provider标签

这个东西是用来定义桌面widget的大小,初始状态等等信息的,它的位置应该放在res/xml文件夹下,具体的xml参数如下:

android:minWidth : 最小宽度
android:minHeight : 最小高度
android:updatePeriodMillis : 更新widget的时间间隔(ms)
android:previewImage : 预览图片
android:resizeMode : widget可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以竖直拉伸
android:widgetCategory : widget可以被显示的位置home_screen表示可以将widget添加到桌面,keyguard表示widget可以被添加到锁屏界面
android:initialLayout : 加载到桌面时对应的布局文件
android:initialKeyguardLayout : 加载到锁屏界面时对应的布局文件


我自己的配置文件如下:res/xml/my_appwidget_info.xml

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/my_appwidget"
android:minHeight="60dp"
android:minWidth="180dp"
android:previewImage="@mipmap/preview"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen|keyguard">
</appwidget-provider>


布局文件:res/layout/my_appwidget.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:background="#33000000"
android:orientation="vertical">

<TextView
android:id="@+id/song_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dip"
android:gravity="center_horizontal"
android:textSize="16sp"
android:text="song name"/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:gravity="center_horizontal"
android:orientation="horizontal" >

<ImageView
android:id="@+id/prev_song"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginRight="8dip"
android:background="@mipmap/car_musiccard_up" />

<ImageView
android:id="@+id/play_pause"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginRight="8dip"
android:src="@mipmap/car_musiccard_play" />

<ImageView
android:id="@+id/next_song"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@mipmap/car_musiccard_down" />
</LinearLayout>
</LinearLayout>


注意:在构造Widget布局时,App Widget支持的布局和控件非常有限,有如下几个:

//App Widget支持的布局:
FrameLayout、LinearLayout、RelativeLayout、GridLayout
//App Widget支持的控件:
AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper


除此之外的所有控件(包括自定义控件)都无法显示,无法显示时,添加出来的widget会显示“加载布局出错”。

AppWidgetProvider类

上面我们通过appwidget-provider标签就可以得到初始化的布局,视图等,但我们的widget要实时更新怎么办,要响应用户操作怎么办,这就需要额外的类来辅助处理了,这个类就是AppWidgetProvider。

由于AppWidgetProvider要接收到当前widget的状态(是否被添加,是否被删除等),所以要接收通知,必然是继承自BroadcastReceiver。

AppWidgetProvider中的广播处理函数如下:(根据不同的使用情况,重写不同的函数)

onUpdate():

onUpdate()在主线程中执行,如果处理需要花费时间多于10秒,处理应在service中完成。3种情况下会调用onUpdate():

(1)在时间间隔到时调用,时间间隔在widget定义的android:updatePeriodMillis中设置;

(2)用户拖拽到主页,widget实例生成。

(3)机器重启,实例在主页上显示

onDeleted(Context, int[]):

当 widget 被删除时被触发。

onEnabled(Context):

当第1个 widget 的实例被创建时触发。也就是说,如果用户对同一个 widget 增加了两次(两个实例),那么onEnabled()只会在第一次增加widget时触发。

onDisabled(Context):

当最后1个 widget 的实例被删除时触发。

onReceive(Context, Intent):

在接收到广播时调用。

我们可以先写个简单的Provider类,后面根据需求慢慢丰富:

public class MyAppWidgetProvider extends AppWidgetProvider {

// widget更新时调用
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
Log.d("hx", "onUpdate: appWidgetIds.length=" + appWidgetIds.length + " appWidgetIds[0]=" + appWidgetIds[0]);
super.onUpdate(context, appWidgetManager, appWidgetIds);
}

// widget被删除时调用
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
Log.d("hx", "onDeleted: appWidgetIds.length=" + appWidgetIds.length + " appWidgetIds[0]=" + appWidgetIds[0]);
super.onDeleted(context, appWidgetIds);
}

// 最后一个widget被删除时调用
@Override
public void onDisabled(Context context) {
Log.d("hx", "onDisabled");
super.onDisabled(context);
}

// 第一个widget被创建时调用
@Override
public void onEnabled(Context context) {
Log.d("hx", "onEnabled");
super.onEnabled(context);
}

// 接收广播的回调函数
@Override
public void onReceive(Context context, Intent intent) {
Log.d("hx", "onReceive");
super.onReceive(context, intent);
}
}


这里不是继承接口,这几个函数并不需要全部重写,要用到哪个就重写哪个。

注册MyAppWidgetProvider

至此我们的Widget还不能添加到桌面,还需要最后一步。前面我们讲到AppWidgetProvider派生自BroadcastReciver,所以要提前注册,BroadcastReciver的注册有两种方法,静态注册和动态注册,因为这里要接收来自系统的消息,而且在程序启动时就开始自动监听,所以需要静态注册。

<!-- 声明widget对应的AppWidgetProvider -->
<receiver android:name=".MyAppWidgetProvider" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/my_appwidget_info" />
</receiver>


(1)接收的action定义为:”android.appwidget.action.APPWIDGET_UPDATE”这表明接收系统发来的有关这个app的所有widget的消息(主要是增加、删除)。

(2)<.meta-data> 指定了 AppWidgetProviderInfo 对应的资源文件:

android:name – 指定metadata名,指定为android.appwidget.provider表示这个data中的数据是AppWidgetProviderInfo 类型的

android:resource – 指定 AppWidgetProviderInfo 对应的资源路径。即,xml/my_appwidget_info.xml。

现在可以把Widget添加到桌面啦,效果图如下:





Widget 交互

上面我们实现了Widget的界面,但此时Widget还不能更新内容和响应用户操作,接下来看看Widget的交互。

发送广播与按钮事件绑定

因为appwidget运行的进程和我们创建的应用不在一个进程中,所以我们也就不能像平常引用控件那样来获得控件的实例。这时候,我们就要靠RemoteViews,直译成中文应该是远程视图, 也就是说通过这个东西我们能够获得不在同一进程中的对象。

先看一下Demo代码,后面慢慢分析:

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
/**构造RemoteViews实例*/
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_appwidget);
//添加点击事件
remoteViews.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song));
remoteViews.setOnClickPendingIntent(R.id.play_pause, getPendingIntent(context, R.id.play_pause));
remoteViews.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song));
// 更新Appwidget
appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);

super.onUpdate(context, appWidgetManager, appWidgetIds);
}

private PendingIntent getPendingIntent(Context context,int resID){
Intent intent = new Intent();
//注意这个intent构造的是显式intent,直接将这个广播发送给MyAppWidgetProvider,使用Action的方式接收不到
intent.setClass(context, MyAppWidgetProvider.class);
intent.setData(Uri.parse("hx:" + resID));
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,intent, PendingIntent.FLAG_UPDATE_CURRENT);
return pendingIntent;
}


首先创建了一个remoteViews实例,然后将其中的按钮与点击事件绑定,后面的参数是一个PendingIntent。下面看看PendingIntent又是个什么。

intent意思是意图,pending表示即将发生或来临的事情。

PendingIntent这个类用于处理即将发生的事情。比如在通知Notification中用于跳转页面,但不是马上跳转。所以我们可以将它理解成一个封装成消息的intent。即这个intent并不是立即start,而是像消息一样被发送出去,等接收方接到以后,再分析里面的内容。

可以看到,PendingIntent是Intent的封装,在构造PendingIntent前,需要先构造一个Intent(注意这里是显式intent,直接将这个广播发送给MyAppWidgetProvider),并可以利用Intent的属性传进去action,Extra等,同样,在接收时,对方依然是接收Intent,而不是接收PaddingIntent。这个问题,我们后面可以看到。

PendingIntent.getBroadcast(context, 0, intent, 0);


指从系统中获取一个用于可以发送BroadcastReceiver广播的PendingIntent对象。所以流程就是我们点击控件,就会发出一个广播,广播携带了Intent,接收方(onReceive)通过Intent判断我们点击的是哪个控件,再作出相应的处理。

这里有一点需要注意:

我们intent传递数据使用的是setData

intent.setData(Uri.parse("hx:" + resID));


那么能不能用putExtra呢?

intent.putExtra("id", resID);


实践发现这种方式取到的id值永远只有一个,即使点击不同的控件也不会变。究其原因是因为没有创建新的PendingIntent,仍然是复用的前一个。如果要创建两个不同的PendingIntent,而不要系统替换前一个,不要仅仅在PutExtra()中包含不同的内容,因为在Extra的不同,并不会用来识别两个不同的PendingInent,要看两个PendingIntent是否相同,可以利用filterEquals (Intent other)来判断两个Intent是否相同,即除了Extra域的任何不同都会标识为两个不同的Intent。所以用setData就标识了两个不同的Intent。还有另外一种方式,使用不同的RequestCode也可以构建新的PendingIntent。

总结一下,两种方式:

利用filterEquals (Intent other)里的那几个域的不同来构造不同的Intent

在构建PendingIntent时使用不同的RequestCode

创建remoteViews实例后,还有一句:

appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);


就是利用updateAppWidget()将构造的RemoteView更新指定的widget界面。注意这里有个appWidgetIds,这个参数是通过OnUpdate()传过来的,它是一个int数组,里面存储了用户所创建的所有widget的ID值。更新的时候也是通过widget的ID值,一个个更新的。

接收广播

由于我们在创建广播的Intent时,使用的显示Intent,所以我们的广播不需要注册就会发到这们这个类(MyAppWidgetProvider.java)里面。

在接收到广播后,我们先判断Intent中是不是包含data,因为我们在发送广播时放data中塞了数据(控件的ID),所以只要data中有数据就可以判定是用户点击控件发来的广播。然后同样利用RemoteView修改textView的文字,代码如下:

@Override
public void onReceive(Context context, Intent intent) {
Uri data = intent.getData();
int resID = -1;
if(data != null) {
resID = Integer.parseInt(data.getSchemeSpecificPart());
}
/**通过远程对象设置文字*/
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_appwidget);
switch (resID) {
case R.id.prev_song:
remoteViews.setTextViewText(R.id.song_name,"prev_song click");
break;
case R.id.play_pause:
remoteViews.setTextViewText(R.id.song_name,"play_pause click");
break;
case R.id.next_song:
remoteViews.setTextViewText(R.id.song_name,"next_song click");
break;
}
// 获得appwidget管理实例,用于管理appwidget以便进行更新操作
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
// 相当于获得所有本程序创建的appwidget
ComponentName componentName = new ComponentName(context, MyAppWidgetProvider.class);
// 更新appwidget
appWidgetManager.updateAppWidget(componentName, remoteViews);
super.onReceive(context, intent);
}


其实RemoteView中的操作控件的方法非常有限,可以看这里:Android App Widget中如何调用RemoteView中的函数

两个地方需要注意:

(1)在OnUpdate中,我们更新界面是通过传过来的widget的id数组来更新所有widget的。而这里是通过获取ComponentName来更新的。其实这里还有另一种实现方式,即我们可以把OnUpdate中传过来的appWidgetIds保存起来,在这里同样使用OnUpdate中的appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);来更新。但我更推荐这里利用ComponentName的这种方式来更新,因为当我们的应用程序进程被杀掉后再起来的时候,会赋予新的appWidgetIds,使用ComponentName这种更新方式还是可以继续响应的,而利用保存appWidgetIds的方式是不会响应的。

(2)在实际项目中,大家可能会想到复用remoteView,即如果已经创建了就不再重新加载layout,而是重新绑定控件及数据,千万不要这样!!!

如果你在绑定数据时涉及图片等大数据,remoteView不会每次清理,所以如果每次都使用同一个remoteView进行传输会因为溢出而绐终无响应! 你看着每次动作都通过updateAppWidget传送出去了,但界面死活就是没响应;而且重装应用程序也不会有任何反应,只能重启手机才会重新有反应,也是醉了。

主要原因在于:Binder data size limit is 512k,由于传输到appWidget进程中的Binder最大数据量是512K,并且RemoteView也不会每次清理, 所以如果每次都使用同一个RemoteView进行传输会因为溢出而报错.所以必须每次重新建一个RemoteView来传输!!!!!!

话不多说,看效果:



实战(音乐播放器)

首先需要一个播放音乐的Service,这里叫做MusicManageService:

public class MusicManageService extends Service {

private MediaPlayer mPlayer;
private int mIndex = 4;// 从中间开始放
private int[] mArrayList = new int[9];
public static String ACTION_CONTROL_PLAY = "action_control_play";
public static String KEY_USR_ACTION = "key_usr_action";
public static final int ACTION_PRE = 0, ACTION_PLAY_PAUSE = 1, ACTION_NEXT = 2;
private boolean mPlayState = false;

//接收MyAppWidgetProvider发过来的广播,控制播放器播放
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action  = intent.getAction();
if (ACTION_CONTROL_PLAY.equals(action)) {
int widget_action = intent.getIntExtra(KEY_USR_ACTION, -1);

switch (widget_action){
case ACTION_PRE:
playPrev(context);
Log.d("hx","action_prev");
break;
case ACTION_PLAY_PAUSE:
if (mPlayState) {
pause(context);
Log.d("hx","action_pause");
}else{
play(context);
Log.d("hx","action_play");
}
break;
case ACTION_NEXT:
playNext(context);
Log.d("hx","action_next");
break;
default:
break;
}
}

}
};

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_CONTROL_PLAY);
registerReceiver(receiver, intentFilter);
initList();
mediaPlayerStart();
}
private void mediaPlayerStart(){
mPlayer = new MediaPlayer();
mPlayer = MediaPlayer.create(getApplicationContext(), mArrayList[mIndex]);
mPlayer.start();
mPlayState = true;
updateUI(getApplicationContext(), MyAppWidgetProvider.UPDATE_UI_PLAY, mIndex);
}

private void initList() {
mArrayList[0] = R.raw.dui_ni_ai_bu_wan;
mArrayList[1] = R.raw.fei_yu;
mArrayList[2] = R.raw.gu_xiang_de_yun;
mArrayList[3] = R.raw.hen_ai_hen_ai_ni;
mArrayList[4] = R.raw.new_day;
mArrayList[5] = R.raw.shi_jian_li_de_hua;
mArrayList[6] = R.raw.ye_gui_ren;
mArrayList[7] = R.raw.yesterday_once_more;
mArrayList[8] = R.raw.zai_lu_shang;
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() {
super.onDestroy();
mPlayer.stop();
unregisterReceiver(receiver);
}

/**
* 播放下一首
* @param context
*/
public void playNext(Context context) {
if (++mIndex > 8) {
mIndex = 0;
}
mPlayState = true;
playSong(context);
updateUI(context, MyAppWidgetProvider.UPDATE_UI_PLAY,mIndex);
}

/**
* 播放上一首
*
* @param context
*/
public void playPrev(Context context) {
if (--mIndex < 0) {
mIndex = 8;
}
mPlayState = true;
playSong(context);
updateUI(context, MyAppWidgetProvider.UPDATE_UI_PLAY, mIndex);
}

/**
* 继续播放
*/
public void play(Context context) {
mPlayState = true;
mPlayer.start();
updateUI(context, MyAppWidgetProvider.UPDATE_UI_PLAY, mIndex);
}

/**
* 暂停播放
*
* @param context
*/
public void pause(Context context) {
mPlayState = false;
mPlayer.pause();
updateUI(context, MyAppWidgetProvider.UPDATE_UI_PAUSE, mIndex);
}

/**
* 播放指定的歌曲
*
* @param context
*/
private void playSong(Context context) {
AssetFileDescriptor afd = context.getResources().openRawResourceFd(mArrayList[mIndex]);
try {
mPlayer.reset();
mPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getDeclaredLength());
mPlayer.prepare();
mPlayer.start();
afd.close();
} catch (Exception e) {
Log.e("hx","Unable to play audio queue do to exception: "+ e.getMessage(), e);
}
}

//发送广播到MyAppWidgetProvider用来改变widget的显示
private void updateUI(Context context, int state, int songId) {
Intent actionIntent = new Intent(MyAppWidgetProvider.ACTION_UPDATE_UI);
actionIntent.putExtra(MyAppWidgetProvider.KEY_UI_PLAY_BTN, state);
actionIntent.putExtra(MyAppWidgetProvider.KEY_UI_TEXT, songId);
context.sendBroadcast(actionIntent);
}
}


Service写完之后记得在Manifest中注册,还有Service需要发广播给MyAppWidgetProvider通知更新UI,所以MyAppWidgetProvider除了响应系统的Action之外,还需要加上自己的Action:

<!-- 声明widget对应的AppWidgetProvider -->
<receiver android:name=".MyAppWidgetProvider" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="action_update_ui" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/my_appwidget_info" />
</receiver>
<!-- 播放音乐service-->
<service android:name=".MusicManageService"/>


然后就是重写MyAppWidgetProvider文件:

public class MyAppWidgetProvider extends AppWidgetProvider {

private boolean mStop = true;
public static String ACTION_UPDATE_UI = "action_update_ui";  //Action
public static String KEY_UI_PLAY_BTN = "ui_play_btn_key"; //putExtra中传送当前播放状态的key
public static String KEY_UI_TEXT = "ui_text_key"; //putExtra中传送TextView的key
public static final int UPDATE_UI_PLAY = 1, UPDATE_UI_PAUSE =2;//当前歌曲的播放状态

// 更新所有的 widget
private void updateRemoteViews(Context context,AppWidgetManager appWidgetManager, String songName, Boolean play_pause) {
RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.my_appwidget);
//将按钮与点击事件绑定
remoteView.setOnClickPendingIntent(R.id.play_pause,getPendingIntent(context, R.id.play_pause));
remoteView.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song));
remoteView.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song));
//设置内容
if (!songName.equals("")) {
remoteView.setTextViewText(R.id.song_name, songName);
}
//设定按钮图片
if (play_pause) {
remoteView.setImageViewResource(R.id.play_pause, R.mipmap.car_musiccard_pause);
}else {
remoteView.setImageViewResource(R.id.play_pause, R.mipmap.car_musiccard_play);
}
// 相当于获得所有本程序创建的appwidget
ComponentName componentName = new ComponentName(context, MyAppWidgetProvider.class);
appWidgetManager.updateAppWidget(componentName, remoteView);
}

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
Log.d("hx", "onUpdate: appWidgetIds.length=" + appWidgetIds.length + " appWidgetIds[0]=" + appWidgetIds[0]);
updateRemoteViews(context, appWidgetManager, "", false);
}

private PendingIntent getPendingIntent(Context context,int resID){
Intent intent = new Intent();
//注意这个intent构造的是显式intent,直接将这个广播发送给MyAppWidgetProvider
intent.setClass(context, MyAppWidgetProvider.class);
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
intent.setData(Uri.parse("hx:" + resID));
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,intent,PendingIntent.FLAG_UPDATE_CURRENT);
return pendingIntent;
}

// 接收广播的回调函数
@Override
public void onReceive(Context context, Intent intent) {
Log.d("hx", "onReceive");

String action = intent.getAction();
Log.d("hx", "action:"+action);

if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) { //手势操作后系统发来的广播
Uri data = intent.getData();
int resID = Integer.parseInt(data.getSchemeSpecificPart());
switch (resID) {
case R.id.play_pause:
controlPlay(context, MusicManageService.ACTION_PLAY_PAUSE);
if(mStop) {
Intent startIntent = new Intent(context, MusicManageService.class);
context.startService(startIntent);
mStop = false;
}
break;
case R.id.prev_song:
controlPlay(context, MusicManageService.ACTION_PRE);
break;
case R.id.next_song:
controlPlay(context, MusicManageService.ACTION_NEXT);
break;
}
} else if (ACTION_UPDATE_UI.equals(action)) { //MusicManageService发来的广播
int play_pause =  intent.getIntExtra(KEY_UI_PLAY_BTN, -1);
int songId = intent.getIntExtra(KEY_UI_TEXT, -1);
switch (play_pause) {
case UPDATE_UI_PLAY:
updateRemoteViews(context, AppWidgetManager.getInstance(context), "current sond id:" + songId, true);
break;
case UPDATE_UI_PAUSE:
updateRemoteViews(context, AppWidgetManager.getInstance(context), "current sond id:" + songId, false);
break;
default:
break;
}
}
super.onReceive(context, intent);
}

//发送广播到MusicManageService控制播放
private void controlPlay(Context context, int ACTION) {
Intent actionIntent = new Intent(MusicManageService.ACTION_CONTROL_PLAY);
actionIntent.putExtra(MusicManageService.KEY_USR_ACTION, ACTION);
context.sendBroadcast(actionIntent);
}
}




Demo下载地址
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: