一文掌握android小组件(AppWidget)开发

本文详细介绍了Android小组件的基础开发,包括创建`ScanWidgetProvider`、配置receiver、资源文件和布局文件,以及如何通过`RemoteViews`实现组件的灵魂——点击事件和数据更新。同时扩展至列表组件的开发,展示了如何使用`ListView`和`GridView`,并揭示了小组件的开发原理和注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在学习android小组件开发,记之以便日后温习。桌面小组件可以方便使用app的隐藏功能,实现一键触达,提升用户体验。这样可以极大便捷家里的老人使用应用,享受到互联网带来的便捷。最近给家里的老人添加了追剧的小组件,这样她不需要学习怎么搜索、怎么选择,通过桌面小组件便可以快速触达。下面将分别介绍基础小组件开发、列表小组件开发、开发小组件的注意事项。

一、基础小组件的开发

实现android的小组件需要按照一定的范式,主要包括四步,下面将以扫一扫的小组件入口为例分别介绍:

第一步:创建ScanWidgetProvider.java类,继承自android的AppWidgetProvider。小组件的核心逻辑包括点击、展示都在此处。后文会详细介绍。目前此类可以保持空实现。

第二步:在AndroidManifest中配置小组件的receiver,了解android的都知道receiver配置的是广播接收器,难道小组件是一个广播接收器?通过查看AppWidgetProvider的继承类发现,其继承了BroadcastReceiver,所以小组件实际上就是一个广播接收器。

<receiver
            android:name="com.demo.widget.ScanWidgetProvider"    //第一步中创建的小组件类名称
            android:exported="true"   //此处需要配置true
            android:label="扫一扫">  //添加引导处展示的标签名称
            <meta-data
                android:name="android.appwidget.provider"   //固定格式
                android:resource="@xml/widget_scan_resource" />   //配置文件
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
 </receiver>

第三步:创建第二步中用到的resource文件widget_scan_resource.xml,创建在res/xml文件下,示例如下。

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/scan_widget_layout"  //布局文件
    android:minHeight="110dp"   //最小高度
    android:minWidth="110dp"    //最小宽度
    android:previewImage="@drawable/scan_demo_image"   //示例图,往往为我们组件的样式图
    android:updatePeriodMillis="3600000" />                           //更新时间间隔,单位是毫秒,

上述代码需重点介绍minHeight和minWidth。手机将桌面划分成了一个个独立网格,然后桌面内容都展示在网格中,一个应用图标就占一格。小组件在桌面同样展示在网格中,当占一个网格时就是1X1的组件;横向两个网格就是2X1的组件,那么横纵各两个网格自然就是2X2的组件。此处的minHeight和minWidth是指需要占用的网格的总长和宽,常用经验公式计算:(m为横向网格数,n为竖向网格数)
minWidth = 70 x m - 30(dp)
minWidth = 70 x n - 30(dp)
此时你可能不禁会问,如果设置的最小宽高和手机网格的宽高不匹配怎么办。没关系,系统会帮取整找到最近的网格倍数设置给小组件。这也是为什么此参数需要加上min(最小)了。

第四步:创建小组件的布局文件scan_widget_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_widget_scan_guide"
    android:layout_width="148dp"
    android:layout_height="148dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/scan_widget_bg">
        <!--图片-->
        <ImageView
            android:id="@+id/iv_scan"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="10dp"
            android:scaleType="fitXY"
            android:src="@drawable/demo_scan" />

        <TextView
            android:id="@+id/tv_scan"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/iv_scan"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="10dp"
            android:text="扫一扫"
            android:textSize="24dp"
            android:textStyle="bold" />
    </RelativeLayout>
</FrameLayout>

至此一个简单的小组件就开发完成了。将以上代码放入项目中,手机中就可以添加扫一扫小组件了。如下图所示
添加小组件示意图添加到桌面样式
目前上述小组件只有空壳,还没有灵魂,所以需要为他注入灵魂。想必你已经猜到,就是在第一步创建的ScanWidgetProvider.java类中注入小组件的“灵魂”。那么小组件的灵魂是什么呢?当然是曝光更能吸引用户的信息,点击后能拉起app到指定落地页。废话不多说,先上代码。

public class ScanWidgetProvider extends AppWidgetProvider {
    private static final String TAG = "ScanWidgetProvider";

    private static RemoteViews remoteViews;

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(TAG, "onReceive");
        super.onReceive(context, intent);
    }

    //小组件更新时回调,所以加载小组件的逻辑都在此处
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        Log.d(TAG, "onUpdate");
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        if (appWidgetManager == null || appWidgetIds == null) {
            Log.d(TAG, "onUpdate error, appWidgetManager=" + appWidgetManager + " appWidgetIds=" + appWidgetIds);
            return;
        }
        //小组件是运行在独立进程中,所以涉及到跨进程的渲染,故只能使用remoteViews加载和设置ui
        createRemoteViews(context);
        updateDefaultWidget(context, appWidgetManager, appWidgetIds);
    }

    private static void updateDefaultWidget(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        int roundCorner = Utils.dip2px(context, 14);    //dp转化为px,工具类代码不贴了,网上很多
        int topWidth = Utils.dip2px(context, 80);
        int topHeight = Utils.dip2px(context, 80);
        //将图片压缩加载
        Bitmap topBitmap = decodeSampleBitmap(context, R.drawable.demo_scan, topWidth, topHeight);
        if (topBitmap!= null) {
            //使用remoteViews设置图片视图
            remoteViews.setImageViewBitmap(R.id.iv_scan, topBitmap);
        }
        //使用remoteViews设置展示文字和颜色
        remoteViews.setTextViewText(R.id.tv_scan, "AR扫一扫");
        remoteViews.setTextColor(R.id.tv_scan, Color.RED);
        updateWidgetClickEvent(context, appWidgetManager, appWidgetIds);
    }

    private static void updateWidgetClickEvent(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        DebugLog.log(TAG, "updateWidgetClickEvent");
         Intent intent = new Intent("com.demo.scan.main");
        intent.setPackage(context.getPackageName());
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        intent.putExtra("WIDGET_NAME", "ScanWidgetProvider");
        
        //remoteViews设置点击事件,只能使用PendingIntent
        intent.putExtra("CLICK_ACTION", "scan_demo_image_click");
        PendingIntent pendingIntent = PendingIntent.getActivity(context, R.id.iv_scan, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        remoteViews.setOnClickPendingIntent(R.id.iv_scan, pendingIntent);
        //渲染UI
        submitRender(appWidgetManager, appWidgetIds, remoteViews);
    }
    //渲染小组件
    private static void submitRender(AppWidgetManager appWidgetManager, int[] appWidgetIds, RemoteViews remoteViews) {
        for (int appWidgetId : appWidgetIds) {
            Log.d(TAG, "submitRender,widgetId:" + appWidgetId);
            appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
        }
    }

    private void createRemoteViews(Context context) {
        if (remoteViews == null) {
            DebugLog.log(TAG, "createRemoteViews");
            remoteViews = new RemoteViews(context.getPackageName(), R.layout.scan_widget_layout);
        }
    }
    
    public static Bitmap decodeSampleBitmap(Context context, int resId, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(context.getResources(), resId, options);
        //计算采样比例
        options.inSampleSize = calculateSampleSize(options, reqWidth, reqHeight);
        DebugLog.log(TAG,"options.inSampleSize:" + options.inSampleSize);
        options.inJustDecodeBounds = false;
        return bitmap = BitmapFactory.decodeResource(context.getResources(), resId, options);
    }
}

public static int calculateSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        //获得图片的原宽高
        int height = options.outHeight;
        int width = options.outWidth;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            // 计算出实际宽高和目标宽高的比率
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            final int widthRatio = Math.round((float) width / (float) reqWidth);
            inSampleSize = Math.min(heightRatio, widthRatio);
        }
        return inSampleSize;
    }

小组件的更新代码都在onUpdate中实现,widget中只能使用RemoteViews提供的一些列set方法更新小组件的内容,例如使用setImageViewBitmap设置图片,使用setTextViewText设置文字,使用setOnClickPendingIntent设置点击事件等。

至此一个基础的小组件开发就完成了,由于小组件展示在“寸土寸金”的桌面,使用基础组件就可以完成绝大部分需求。但是如果碰到需要展示列表形式,则需要使用ListView、GridView。

二、列表组件的开发

通过上文介绍知道小组件并不能像普通安卓应用开发那样,找到指定view,然后对view进行操作。小组件需要通过RemoteViews提供的一些列的set方法对视图渲染更新。那么listview和gridview也是一样的。回忆一下列表视图的正常开发流程,先创建listview的视图,然后为视图设置layoutmanager设置布局样式,最后创建列表的adapter进行数据填充。widget采用同样的实现流程,只是需要使用符合widget规范的特有方式实现。接下来将在上文案例的基础上进行修改:

第一步:修改widget_scan_resource.xml的最小宽高,设置为4X4的组件

android:minHeight="250dp"   //最小高度
android:minWidth="250dp"    //最小宽度

第二步:修改布局文件scan_widget_layout.xml,在命名为tv_scan的Textview下增加girdlist

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_widget_vip_guide"
    android:layout_width="296dp"
    android:layout_height="296dp">
    .....
	<GridView
	            android:id="@+id/scan_list"
	            android:layout_width="match_parent"
	            android:layout_height="match_parent"
	            android:layout_below="@id/tv_scan"
	            android:layout_alignParentBottom="true"
	            android:horizontalSpacing="4dp"
	            android:numColumns="auto_fit"
	            android:verticalSpacing="4dp" />
	......
</FrameLayout >

第三步:创建更新列表视图需要的“adapter”,此处打了双引号,就是说需要按照小组件的方式实现,下面两个类按照固定范式实现。

public class ListWidgetService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new ListWidgetFactory(this);
    }
}

public class ListWidgetFactory implements RemoteViewsService.RemoteViewsFactory {

    private final List<String> data = new ArrayList<>();
    private final Context context;

    public ListWidgetFactory(Context context) {
        this.context = context;
    }

    @Override
    public void onCreate() {
        initData();
    }

    private void initData() {
        data.clear();
        for (int i = 0; i < 10; i++) {
            Random random = new Random();
            String msg = "扫一扫" + random.nextInt(101);
            data.add(msg);
        }
    }

    @Override
    public void onDataSetChanged() {

    }

    @Override
    public void onDestroy() {
        data.clear();
    }

    @Override
    public int getCount() {
        return data.size();
    }

//重点实现
    @Override
    public RemoteViews getViewAt(int position) {
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.list_scan_item);

        remoteViews.setTextViewText(R.id.list_item_tv, data.get(position));

        remoteViews.setImageViewResource(R.id.list_item_image, R.drawable.demo_scan);

        Intent intent = new Intent();
        intent.putExtra("CLICK_POSITION", position);
        remoteViews.setOnClickFillInIntent(R.id.list_item_root, intent);
        return remoteViews;
    }

    @Override
    public RemoteViews getLoadingView() {
        return null;
    }

    @Override
    public int getViewTypeCount() {
        return 1;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }
}

这一步代码有点长,但是逻辑很简单。就是先创建RemoteViewsService ,通过此service返回一个RemoteViewsFactory ,RemoteViewsFactory 就是通常理解的列表更新的adapter,其实从内部的继承方法也可以看出。getItemId、getViewTypeCount、getCount是不是都似曾相识?没错,就是开发列表adapter常复写的方法。
此处重点介绍一下getViewAt,内部同样需要使用RemoteViews实现单个列表的view,列表内元素的信息更新需要使用RemoteViews提供的每个set方法。list_scan_item视图布局代码不再提供,很简单就是上图下文的形式。

第四步:更新ScanWidgetProvider,主要在onUpdate方法中设置列表的点击事件和“adapter”

//        设置列表的adapter
        Intent listIntent = new Intent(context, ListWidgetService.class);
        remoteViews.setRemoteAdapter(R.id.scan_list, listIntent);
//设置列表的点击事件
		Intent intent1 = getCommenIntent(context);
        intent1.putExtra("CLICK_ACTION", "scan_demo_list_click");
        PendingIntent pendingIntent1 = PendingIntent.getActivity(context, R.id.scan_list, intent1, PendingIntent.FLAG_UPDATE_CURRENT);
        remoteViews.setPendingIntentTemplate(R.id.scan_list, pendingIntent1);

上述代码需重点关注列表的点击事件,setPendingIntentTemplate是设置列表的通用点击Intent,然后单个item的点击如果需要增加参数,则需使用setOnClickFillInIntent设置补充信息。接收方收到的intent中包含这两部分的内容,于是便可以区分出具体是哪个item的点击了。至此一个列表的小组件便实现完成了。

三、小组件的开发原理和注意事项

1、小组件实际上是运行在系统进程中,所以和app进程涉及到跨进程通信,只能使用RemoteViews更新数据,使用PendingIntent执行延时意图。

2、RemoteViews支持的布局只有以下这些,开发的小组件布局不能超出这些范围。
布局:FrameLayout、LinearLayout、RelativeLayout、GridLayout。
View:Button、TextView、Chronometer、ImageButton、ImageView、ProgressBar、ViewFlipper、AnalogClock、AdapterViewFlipper、ViewStub、ListView、GridView、StackView

3、小组件类继承自AppWidgetProvider,实际上就是一个广播,可以通过发送广播和接受广播进行通信更新,广播接受在onReceive中实现。
onUpdate :小组件创建时和达到更新周期时触发。
onEnabl:第一个改类型小组件添加时触发。
onDeleted:每次删除该小组件时触发。
onDisabled:最后一个该类型小组件添加时触发。

4、在AndroidManifest注册小组件必须要有android.appwidget.action.APPWIDGET_UPDATE这个action,否则将无法安装,其他的action和自定义的action可根据需要添加。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值