Android App Widget 开发

概述

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:resizeModewidget可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以竖直拉伸
android:widgetCategorywidget可以被显示的位置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


<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的注册有两种方法,静态注册和动态注册,因为这里要接收来自系统的消息,而且在程序启动时就开始自动监听,所以需要静态注册。

        
        <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添加到桌面啦,效果图如下:
Android App Widget 开发_第1张图片 Android App Widget 开发_第2张图片

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来传输!!!!!!

话不多说,看效果:
Android App Widget 开发_第3张图片

实战(音乐播放器)

首先需要一个播放音乐的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:

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

Android App Widget 开发_第4张图片

Demo下载地址

你可能感兴趣的:(UI设计)