AppWidget实现自定义view

一、鸡汤

appwidget是android中小组件,我们经常说的widget其实是指的那些button、textview、imageview等这些小控件,而appwidget则是嵌入到别的app中的activity中显示的一种视图。通常我们的appwidget都是嵌入到luncher应用中的(我们经常说的桌面其实也是一款app也就是home luncher应用,手机里的应用会在其activity内显示一个启动图标),运行在luncher应用中,而其事件处理都是在本app内的进程中完成的,所以这里就会涉及到跨进程通信,而如果本应用想要跟appwidget的视图所运行的app通信,因为appwidget运行在别的进程中,只能使用remoteview去更新视图,remoteview相比于view具有跨进程的能力但是其支持的视图也是非常有线的,常用的大概就是textview、imageview、imagebutton、button、listview、gridview,其余的像自定义view、recycleview等等都是不支持的,所以appwidget的功能还是非常有限的。如果我们只是使用像textview这种没有子布局的控件那么使用方式是非常简单的,这里会涉及到:AppWidgetProvider、AppWidgetProviderInfo这两个类。而如果涉及到像listview、gridview这种,还会涉及到RemoteViewsService和RemoteViewsFactory。


AppWidgetProvider:该类继承自broadcastreceiver,需要在清单文件中注册<receiver>标签。

AppWidgetProviderInfo:该类只需写xml文件就可以了,xml放在res/xml下面,跟标签为<appwidget-provider>.

RemoteViewsService:该类继承自service,需要在清单文件中注册。

RemoteViewsFactory:该类为RemoteViewsService的内部类,处理service中的工作。


ok,有了上面的大体介绍,下面我们就这两种情况具体来介绍一下。


只含有textview、button等情况

首先我们要准备一个appwidget用来显示的布局文件,这里我们假定只有一个button,放在layout下面就ok,取名为widget_layout,具体代码就补贴出来了,你自己决定写什么样的效果,不过只能含有我上面说的哪几种情况哦。接下来我们需要准备一个AppWidgetProviderInfo类,上面知道这个类是在res/xml下面新建一个xml文件来自动生成的,具体代码如下类似:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/widget_layout"
    android:minHeight="300dp"
    android:minResizeHeight="90dp"
    android:minResizeWidth="190dp"
    android:resizeMode="horizontal|vertical"
    android:minWidth="200dp">

    </appwidget-provider>
简要的介绍下属性值:
initialLayout:指定把appwidget放置在桌面上的时候的初始布局。

minHeight、minResizeHeight:前一个是指定appwidget的最小高度,后一个指定在允许重新测量appwidget高度的情况下的最少高度,后一个值必须要小于前一个值,否则其会被忽略。

resizeMode:允许在哪些方向上重新测量,我们都知道appwidget在桌面显示的时候会有横向和纵向上有多少个格子,比如2*2、4*4,系统就是根据其能提供的类型和minHeight的值算出一个新的ResizeHeight,如果这个值小于miniResizeHeight就会自动在对应方向上增加一个格子的大小。

其还有下面的属性:

android:configure:指定appwidget第一次放置在桌面上面的时候需要打开的activity,如果配置了这个属性下面将讲到的AppwidgetProvider中的onUpdate就不会在第一次被调用,以后的添加才执行改调用。

android:previewImage:指定appwidget在预览界面显示的图片,也就是我们选择appwidget的时候显示的界面,如果不设置改属性系统就会给app的icon图标作为预览。

 android:widgetCategory="home_screen|searchbox|keyguard":用来设置appwidget可以在那些情况下显示,默认只能在home——screen上面显示。keyguard用来在锁屏的时候显示,searchbox实在搜索页面添加。

 android:updatePeriodMillis:自动更新的时间,最少为半个小时。

ok,配置好上面的类之后我们来看看AppWidgetProvieder这个类,前面说了该类是继承自BroadcastReceiver,所以我们要在清单文件中配置<receiver>标签,如下所示:

  <receiver android:name=".WidgetProviderClass">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"></action>
                <action android:name="com.xinxue.action.TYPE_BTN"></action>
                <action android:name="com.xinxue.action.TYPE_LIST"></action>
            </intent-filter>
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/widget_provider"></meta-data>
        </receiver>

这里的<meta-data>是必须要写的,名字是固定的,resource就是我们创建的上面的那个xml文件的位置。其中
 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"></action>

这个是必须要写的一个action,用来接收appwidget更新的广播,后两个为我自己写的广播。下面我们来看看AppwidgetProvider中的代码:

public class WidgetProviderClass extends AppWidgetProvider {
 
}

其实我们只要继承它就可以了,里面的代码我们可以都不写,这样运行就会正常的使用appwidget了。前提是我们的widget的视图必须是没有包含子视图的布局,比如只有一个textview,去你的appwidget里面选择我们的小组件放在桌面,这里我贴一下运行的效果:



也许你的效果和我的不一样,那取决于你定义的appwidget的布局文件的效果。ok,简单的效果是有了,如果你想要给按钮添加上点击事件,我们只需要在appwidgetprovider的类中重写onUpdate方法,如下类似:

public class WidgetProviderClass extends AppWidgetProvider {
    public static final String BTNACTION = "com.xinxue.action.TYPE_BTN";

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
        //创建一个广播,点击按钮发送该广播
        Intent intent = new Intent(BTNACTION);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        remoteViews.setOnClickPendingIntent(R.id.widget_btn, pendingIntent);
        //如果你添加了多个实例的情况下需要下面的处理
        for (int i = 0; i < appWidgetIds.length; i++) {
            appWidgetManager.updateAppWidget(appWidgetIds[i], remoteViews);
        }
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        switch (intent.getAction()) {
            case BTNACTION:
                Toast.makeText(context, "点到我啦!", Toast.LENGTH_SHORT).show();
                break;


        }
        super.onReceive(context, intent);
    }
}
运行之后点击你的按钮就可以看到效果啦。我简单的给你介绍下上面的代码,在onUpdate方法中首先创建一个remoteview实例,再创建一个带有指定action的Intent,然后通过这个intent创建一个能发送广播的pendingIntent,然后调用remoteview的setOnClickPendingIntent方法绑定一个点击事件,点击按钮的时候会发送带有前面action的广播,要主要的是该方法的第一个参数为要点击的view的id。因为前面我们在清单文件中已经注册过这种类型的广播,在这里就可以收到该广播了,我们只需要重写其onReceiver方法,在里面过滤出我们需要的广播,在这里你就可以实现自己的点击逻辑了,比如打开app的首页。


带有listview的情况


我们修改自己的布局文件,添加一个listview,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/kk"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

 <ListView
   android:id="@+id/widget_listview"
     android:background="#000"
     android:layout_width="match_parent"
     android:layout_height="match_parent">

 </ListView>

</LinearLayout>

OK,前面我们说到,要使用这种带有子布局的情况需要使用remoteviewsService和RemoteViewsFactory,那么我们新建一个类叫RemoteviewsServiceImp,让它继承与RemoteviewsService,然后实现里面的方法,不要忘记在manifest文件里面添加<service>标签,标签内的内容如下:

 <service
            android:name=".RemoteViewServiceImp"
            android:permission="android.permission.BIND_REMOTEVIEWS"></service>

**********注意权限哦!

因为remoteviewsservice的任务都是交给factory去完成的,这里我们就建立一个内部类让它实现remoteviewsfactory接口,然后重写里面的方法,完成后的代码如下:

package com.example.leixinxue.widgettest;

import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;

import java.util.ArrayList;

/**
 * Created by leixinxue on 16-8-8.
 */

public class RemoteViewServiceImp extends RemoteViewsService {

    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new RemoteviewFactoryImp();
    }

    class RemoteviewFactoryImp implements RemoteViewsFactory {
        @Override
        public void onCreate() {

        }

        @Override
        public void onDataSetChanged() {

        }

        @Override
        public void onDestroy() {

        }

        @Override
        public int getCount() {
            return 0;
        }

        @Override
        public RemoteViews getViewAt(int position) {
            return null;
        }

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

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

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

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

OK,下面我们就来写里面具体的代码。首先是让listview传到remoteviewsservice这里来,然后通过它传给remoteviewsfactory,在factory里面就是填充listview的布局内容,我们现在建立一个listview的item的布局文件,里面的代码如下:

<?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:orientation="vertical">

    <TextView
        android:id="@+id/item_textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginBottom="5dp"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="5dp"
        android:text="TextView"
        android:textColor="#fff"
        android:textSize="18sp" />
</LinearLayout>

下面我们来到APPwidgetprovider里面的onupdate方法里面修改代码,修改后的代码如下:

 @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout);

//绑定service用来填充listview中的视图
        Intent intent = new Intent(context, RemoteViewServiceImp.class);
        remoteViews.setRemoteAdapter(R.id.widget_listview, intent);


        //如果你添加了多个实例的情况下需要下面的处理
        for (int i = 0; i < appWidgetIds.length; i++) {
            appWidgetManager.updateAppWidget(appWidgetIds[i], remoteViews);
        }
    }

我们来到factory里面绑定listview的视图,完成后的代码如下:

package com.example.leixinxue.widgettest;

import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;

import java.util.ArrayList;

/**
 * Created by leixinxue on 16-8-8.
 */

public class RemoteViewServiceImp extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new RemoteViewsFactoryImp(this, intent);
    }

    private static ArrayList<String> data;

    public static void loadData() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                data = HttpUtils.getData();
            }
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    class RemoteViewsFactoryImp implements RemoteViewsFactory {
        private Intent requestIntent;
        private Context requestContext;


        public RemoteViewsFactoryImp(Context context, Intent intent) {
            requestContext = context;
            requestIntent = intent;
        }

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

        @Override
        public void onDataSetChanged() {

        }

        @Override
        public void onDestroy() {

        }

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

        @Override
        public RemoteViews getViewAt(int position) {
            RemoteViews remoteViews = new RemoteViews(requestContext.getPackageName(), R.layout.widget_item_layout);

            remoteViews.setTextViewText(R.id.item_textView, data.get(position));
            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 false;
        }
    }
}

这里我使用了一个类来加载网上的数据,当然你可以用自己的方式去现实数据,这里可能有个方法大家不是很懂,就是那个tread.join()方法,它的作用是在线程执行完run方法之后再执行join后面的代码,我这里使用的目的是做有个同步,也就是在数据下载完成后再执行后面的代码。工具类这里就不贴代码了。

到这里我们就可以运行看下效果了。我的运行效果如下:


OK,下面我们给listview加上交互,给每一个item添加上点击事件,需要做需改provider和factory里面的代码,修改完成后的代码如下:

provider中的代码:

package com.example.leixinxue.widgettest;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
import android.widget.Toast;

/**
 * Created by leixinxue on 16-8-8.
 */

public class WidgetProviderClass extends AppWidgetProvider {
    public static final String BTNACTION = "com.xinxue.action.TYPE_BTN";
    public static final String ITEMCLICK = "com.xinxue.action.TYPE_LIST";

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout);

//绑定service用来填充listview中的视图
        Intent intent = new Intent(context, RemoteViewServiceImp.class);
        remoteViews.setRemoteAdapter(R.id.widget_listview, intent);
//添加item的点击事件
        Intent intent1 = new Intent(ITEMCLICK);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 1, intent1, PendingIntent.FLAG_CANCEL_CURRENT);
        remoteViews.setPendingIntentTemplate(R.id.widget_listview, pendingIntent);


        //如果你添加了多个实例的情况下需要下面的处理
        for (int i = 0; i < appWidgetIds.length; i++) {
            appWidgetManager.updateAppWidget(appWidgetIds[i], remoteViews);
        }
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        if (intent.getAction().equals(ITEMCLICK)) {
            Toast.makeText(context, intent.getIntExtra("position", 0) + "", Toast.LENGTH_SHORT).show();
        }
    }
}
factory中的代码:

package com.example.leixinxue.widgettest;

import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;

import java.util.ArrayList;

/**
 * Created by leixinxue on 16-8-8.
 */

public class RemoteViewServiceImp extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new RemoteViewsFactoryImp(this, intent);
    }

    private static ArrayList<String> data;

    public static void loadData() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                data = HttpUtils.getData();
            }
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    class RemoteViewsFactoryImp implements RemoteViewsFactory {
        private Intent requestIntent;
        private Context requestContext;


        public RemoteViewsFactoryImp(Context context, Intent intent) {
            requestContext = context;
            requestIntent = intent;
        }

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

        @Override
        public void onDataSetChanged() {

        }

        @Override
        public void onDestroy() {

        }

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

        @Override
        public RemoteViews getViewAt(int position) {
            RemoteViews remoteViews = new RemoteViews(requestContext.getPackageName(), R.layout.widget_item_layout);
            //listview的点击事件
            Intent intent = new Intent(WidgetProviderClass.ITEMCLICK);
            intent.putExtra("position", position);
            remoteViews.setOnClickFillInIntent(R.id.item_textView, intent);


            remoteViews.setTextViewText(R.id.item_textView, data.get(position));
            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 false;
        }
    }
}

我们运行看下效果,OK,完美。到这里你可能已经学完了APPwidget的教程了,看Google的官方API你可能学到的也就只有这些。


全球首创让你的APPwidget实现自定义view

博客地址:blog.csdn.net/qq379454816------>欢乐斗佛的博客

我们都知道,产品经理大部分是SB,他们不知道怎么去编程,不知道一个模块的功能怎么去实现,可是他们为了酷炫,就是设计出一些很难实现的效果图,如果它设计一个很难得效果图,而这个效果图无法使用基本的视图去实现,必须要用自定义的方式去实现,而APPwidget又不支持自定义view,难道我们用修改底层framework的方式去实现?这当然是一种解决问题的办法,但是这个通常是无法实现的,于是乎你就百度一下,高级一点的屌丝程序员可能Google一下,结果很让人懵逼啊,能搜到几个条目告诉你怎么用自定义view去实现,可是打开一看~~~~~MD,修改framework,额,再次懵逼~~~。绝境之中遇贵人这种桥段是电视惯用的伎俩,但也不是现实生活中没有这种情况,下面我就教你一种全宇宙独创的方式去实现一种自定义view来实现APPwidget无法实现自定义view的窘境,这种方式简单的一B啊,可是如果我不告诉你,你却一辈子也无法去实现,而一旦思路打开,你的奇思妙想就会如尿崩一发而不可收拾,那就是自定义图片!!!什么?这~~~~~。

我们知道APPwidget是可以使用imageview的,而remoteview中有一个方法可以实现替换imageview中的图片:remoteViews.setImageViewBitmap(int viewid,Bitmap bitmap);两个参数,第一个为我们的imageview的id,第二个就是一个图片,imageview我们不可以动手脚,可是这个bitmap的来源我们就可以自己去把控了,你可以使用一个图片利用bitmapfactory来转换,可以使用xml文件来定义一个图片,最大自由度的使用方式是自定义一个bitmap,然后在这个bitmap上面实现我们的复杂效果。考虑到这篇文章前前后后的写了快一个月了,中间还有好多效果图没有给出,而工作忙的我实在没有大片的时间去完善这篇文章,不过中间的步骤什么的我是描述清楚了,上面两种方式的实现百度上面也有相应的文章,这里就部打算去完善了,重点给这个自定义的东西写一下,大家如果对前面的东西还有不明白的地方可以给我留言或者私信。


ok,废话说了一大篇,该是进入正题了。要实现一个自定义的bitmap,我们首先想到是继承 bitmap(有想到继承view的可以在下面排个队,到我这里领赏),可是当继承它的时候你会发现报错了,鼠标一上去一看~~额,bitmap是final类型的,你妹哦!android中图片还有一种方式就是drawable,该方法可以正常使用,不过其尺寸什么的不好去控制,转换bitmap的时候也好麻烦,这里我们就不用该方法了,我们知道自定义都离不开canvas,而构建一个canvas的时候其构造方法中可以传入一个bitmap,那我们何不就此入手实现我们的效果图!这里我说下思路,canvas就好比一个画布,而bitmap就好比一个布,我们给画布上面的布替换成bitmap这块布,然后所有的东西都画到这个布上面,这样bitmap就有内容了,再拿到这个bitmap我们就可以实现我们的目的了。大概就是这样一个过程,我先贴上我在乐视管家项目中的效果图:




录了一个视频,本来想上传给大家看的,奈何csdn不让传,本来想转成gif的,结果它必须要小于2M才可以上传,转换之后看不清就算了,这里就简单的给家描述一下吧。这个小组件分为3个模块,分别监测手机流量、内存、存储三个的使用情况,会动态的刷新,会随着主题的变化切换对应的颜色。实现思路就是使用一个service每2秒刷新一次,上面的视图都是3个imageview,而imageview的图片是使用画笔绘制到一个bitmap上的,因为设计到一些隐私,这里就不把所有的代码分享给大家了,就给一些必要的代码贴出来给你们吧。

首先这个是管家app的小组件,我们来看下他的provider里面的代码:

public class SMWidgetProvider extends AppWidgetProvider {
    private static final String TAG = "smw";
    public static final String FILE_WIDGET_NAME = "widgetId.txt";
    public static final String SP_BASE_KEY = "widgetid_";
    private static final String ACTION_WALLPAPER_COLOR_CHANGE = "com.android.launcher3.WALLPAPER_MASTER_COLOR_CHANGE";//壁纸改变广播
    private static final String EXTRA_WHITE_WALLPAPER = "whiteWallpaper";
    public static final String ISWHITEWALLER_FILENAME = "isWhiteWaller";
    public static final String ISWHITEWALLER = "isWhiteWaller";

    //小组件更新的时候调用的方法
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        WidgetUtils.savaToFile(context, appWidgetIds);//存储id值
        RVSet.getFlowState(context);//根据卡的情况显示对应的视图
    }

    @Override
    public void onEnabled(Context context) {
        context.startService(new Intent(context, WidgetUpdateService.class));
        super.onEnabled(context);
    }

    @Override
    public void onDisabled(Context context) {
        context.stopService(new Intent(context, WidgetUpdateService.class));
        context.getSharedPreferences(FILE_WIDGET_NAME, Context.MODE_PRIVATE).edit().clear();
        super.onDisabled(context);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        RVSet.toDrawText = WidgetUtils.getUnit(context);
        RVSet.memoryInfo = WidgetUtils.getMemoryInfo(context);
        super.onReceive(context, intent);
        String action = intent.getAction();
        if (ACTION_WALLPAPER_COLOR_CHANGE.equals(action)) {
            context.getSharedPreferences(ISWHITEWALLER_FILENAME, Context.MODE_PRIVATE).edit()
                    .putBoolean(ISWHITEWALLER, intent.getBooleanExtra(EXTRA_WHITE_WALLPAPER, false)).commit();
            // 更新UI
            onUpdate(context, AppWidgetManager.getInstance(context), WidgetUtils.readWidgetId(context));
        }
        if (WidgetUpdateService.ACTION_SUPERMANAGER_UPDATE.equals(action)) {
            // 更新UI
            onUpdate(context, AppWidgetManager.getInstance(context), WidgetUtils.readWidgetId(context));
            Log.d(TAG, "get smwidgetprovider broadcast!");
        }
    }


    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        super.onDeleted(context, appWidgetIds);
        //清除id值
        SharedPreferences sp = context.getSharedPreferences(FILE_WIDGET_NAME, Context.MODE_PRIVATE);
        for (int id : appWidgetIds) {
            sp.edit().remove(SP_BASE_KEY + id).commit();
        }
    }
}

这里我们只需要看RVSet这个类,别的都是有些启动服务啊存储小组件id什么的,因为要在别的类中使用小组件id,以及开机的时候还能更新小组件,所已使用sharedpreferences来存储。而RVSet是一个工具类,用来处理收到广播的一些逻辑,以及点击事件的设置等,这里我们调用了 RVSet.getFlowState(context),那么我们来看看里面的代码:

  public static void getFlowState(final Context context) {
        //读取壁纸的值
        isWhiteWaller = context.getSharedPreferences(SMWidgetProvider.ISWHITEWALLER_FILENAME, Context.MODE_PRIVATE).getBoolean(SMWidgetProvider.ISWHITEWALLER, false);
        textColor = isWhiteWaller ? Color.BLACK : Color.WHITE;
        appWidgetManager = AppWidgetManager.getInstance(context);
        appWidgetIds = WidgetUtils.readWidgetId(context);
        remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_main_layout);
        if (!PermissionUtil.isPermissionPhone(context)) {
            //无授权情况
            Log.d(TAG, "getFlowState-->no permission");
            CURRENT_SIM_TYPE = SIM_TYPE_NO_PERMISSION;
            drawMemoryAndStorage(context);
            startUpdate();
            return;
        }
        getSimCount(context, TelephonyUtil.getActiveSIMCount());//标志卡的数量
    }
isWhiteWaller是判断手机当前的壁纸是亮色的还是暗色的,让后更新对应的颜色,getSimCount()该方法用来判断手机当前是几张卡,startUpdate()用来更新桌面的视图,我们来看下里面的代码:

 //更新
    private static void startUpdate() {
        for (int id : appWidgetIds) {
            appWidgetManager.updateAppWidget(id, remoteViews);
        }
    }

直接使用上面的appwidgetIds来更新视图,重点是drawMemoryAndStorage()这个方法,该方法用来绘制后面的那两个模块,也就是内存和存储的模块,我们来看下里面的代码:
//绘制内存和存储的状态
    private synchronized static void drawMemoryAndStorage(Context context) {
        //管家的Logo随着主题更改
        remoteViews.setImageViewBitmap(R.id.widget_img_logo, WidgetUtils.DrawableToBitmap(new OnAppIconLoad(context.getPackageName()).load(context)));
        //绘制图片
        Bitmap bitmapMemory = DrawUtils.TYPE_MEMORY.draw(context, WidgetUtils.getMemoryRat(), memoryInfo[0], memoryInfo[1]);
        Bitmap bitmapStorage = DrawUtils.TYPE_STORAGE.draw(context, WidgetUtils.storageUsedRat(), toDrawText[0], toDrawText[1]);
        //更新图片
        remoteViews.setImageViewBitmap(R.id.widget_imgv_memory, bitmapMemory);
        remoteViews.setImageViewBitmap(R.id.widget_imgv_storage, bitmapStorage);
        //设置字体的颜色
        setViewColor();
        //设置点击事件
        setBtnClick(context);
        startUpdate();
    }

  方法:remoteViews.setImageViewBitmap(R.id.widget_imgv_memory, bitmapMemory);就是把绘制的视图更新到对应的imageview上面去,也就是利用这个方法的第二个参数,我们实现了自定义view。DrawUtils这个类也就是具体去绘制的类,该类我使用类枚举类来实现,里面的代码就是具体绘制3个圆圈,我给整个类的代码分享给大家吧,也算对开源事业做些贡献。代码如下:

public enum DrawUtils {
    TYPE_FLOW, TYPE_MEMORY, TYPE_STORAGE;
    private float mProcess;//绘制的进度
    private String mInfo, unit;//绘制的文本信息
    private static int width, height;//圆环的宽高
    private RectF mRectF;
    private Bitmap bm, bitmap;
    private Canvas canvas;
    private Paint mPaint;
    private Context context;

    //传入一些必要的信息
    public Bitmap draw(Context context, float process, String info, String unit) {
        mProcess = process;
        this.context = context;
        this.unit = unit;
        mInfo = info;
        init();
        //判断是那种类型的需求,然后调用对应的方法绘制
        switch (this) {
            case TYPE_FLOW:
                bm = drawTypeFlow();
                break;
            case TYPE_MEMORY:
                bm = drawTypeMemory();
                break;
            case TYPE_STORAGE:
                bm = drawTypeStorage();
                break;
        }
        return bm;
    }

    //初始化操作
    private void init() {
        int circleWidth = DensityUtil.dip2px(context, 2);
        width = height = DensityUtil.dip2px(context, 80);
        mRectF = new RectF(circleWidth, circleWidth, width - circleWidth, height - circleWidth);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setDither(true);
        mPaint.setStrokeWidth(circleWidth);
        mPaint.setStyle(Paint.Style.STROKE);
        int c = RVSet.isWhiteWaller ? Color.argb(26, 0, 0, 0) : Color.argb(100, 255, 255, 255);
        mPaint.setColor(c);
        mPaint.setFilterBitmap(false);
        bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        canvas = new Canvas(bitmap);
    }

    //绘制剩余流量
    private Bitmap drawTypeFlow() {
        // 绘制圆圈,进度条背景
        canvas.drawArc(mRectF, 0, 360, false, mPaint);
        mPaint.setColor(RVSet.CIRCLE_COLOR);
        canvas.drawArc(mRectF, 270, mProcess * 360, false, mPaint);
        drawText(canvas);
        return bitmap;
    }


    //绘制内存情况
    private Bitmap drawTypeMemory() {
        mPaint.setStrokeWidth(DensityUtil.dip2px(context, 4));
        float dashWidth = DensityUtil.dip2px(context, 3) - 0.5f;//因为DensityUtil工具在转换的时候多加了0.5像素导致出现刻度
        int totalCount = (int) Math.ceil(310 / dashWidth);//算出需要绘制的个数
        DashPathEffect dash = new DashPathEffect(new float[]{DensityUtil.dip2px(context, 1) - 0.5f, DensityUtil.dip2px(context, 2) - 0.5f}, 0);
        mPaint.setPathEffect(dash);
        float drawLength = (float) (Math.ceil(mProcess * totalCount) * dashWidth);//剩余部分
        canvas.drawArc(mRectF, 115, totalCount * dashWidth - drawLength, false, mPaint);
        mPaint.setColor(Color.parseColor("#00fe8f"));
        canvas.drawArc(mRectF, 115 + totalCount * dashWidth - drawLength, drawLength, false, mPaint);
        drawText(canvas);
        return bitmap;
    }

    //绘制存储情况
    private Bitmap drawTypeStorage() {
        //每一份的宽度,总共分了8份
        double v = mRectF.width() * Math.PI / 8;
        DashPathEffect dash = new DashPathEffect(new float[]{(float) (v - DensityUtil.dip2px(context, 1)), DensityUtil.dip2px(context, 1)}, 0);
        mPaint.setPathEffect(dash);
        canvas.drawArc(mRectF, 270, 360, false, mPaint);
        mPaint.setColor(Color.parseColor("#acfa15"));
        canvas.drawArc(mRectF, 270, 360 * mProcess, false, mPaint);
        drawText(canvas);
        return bitmap;
    }

    /**
     * 因为要绘制两遍,而两遍的文字不一样大,所以需要测量两遍字体的高度
     *
     * @param canvas
     */
    private void drawText(Canvas canvas) {
        int c = RVSet.isWhiteWaller ? Color.BLACK : Color.WHITE;
        mPaint.setColor(c);
        mPaint.setStyle(Paint.Style.FILL);
        overRun();//判断是否超限
        //上面的字体高度
        mPaint.setTextSize(DensityUtil.dip2px(context, 20));
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        int textWidth = (int) mPaint.measureText(mInfo, 0, mInfo.length());
        //下面的字体高度
        mPaint.setTextSize(DensityUtil.dip2px(context, 12));
        Paint.FontMetrics fontMetrics1 = mPaint.getFontMetrics();
        int textWidth1 = (int) mPaint.measureText(unit, 0, unit.length());
        //绘制数字
        float theY = mRectF.centerY() - fontMetrics.descent + (fontMetrics.bottom - fontMetrics.top) / 2 - (fontMetrics1.descent - fontMetrics1.ascent) / 2;
        mPaint.setTextSize(DensityUtil.dip2px(context, 20));
        canvas.drawText(mInfo, width / 2 - textWidth / 2, theY, mPaint);
        //绘制单位
        mPaint.setTextSize(DensityUtil.dip2px(context, 12));
        float newY = theY + DensityUtil.dip2px(context, 4) + fontMetrics.bottom - fontMetrics1.descent + (fontMetrics1.descent - fontMetrics1.ascent) / 2;
        canvas.drawText(unit, width / 2 - textWidth1 / 2, newY, mPaint);
    }

    //超限的情况
    private void overRun() {
        switch (this) {
            case TYPE_STORAGE:
                if ((unit.equalsIgnoreCase("M") && Float.parseFloat(mInfo) < 200) || unit.equalsIgnoreCase("K") || unit.equalsIgnoreCase("B"))
                    mPaint.setColor(Color.parseColor("#ff840b"));
                break;
            case TYPE_MEMORY:
                if ((unit.equalsIgnoreCase("M") && Float.parseFloat(mInfo) < 100) || unit.equalsIgnoreCase("K") || unit.equalsIgnoreCase("B"))
                    mPaint.setColor(Color.parseColor("#ff840b"));
                break;
            case TYPE_FLOW://流量超限
                break;
        }
    }

}

3个圆圈的绘制没有超过200行代码,大家看起来应该不是很费劲,步骤就是这样的,首先我们创建了一个矩阵,利用它我们就可以画一个圆,这里我要告诉大家的是,安卓中的圆和椭圆都是使用矩形画内切圆来实现的,然后那个bitmap是使用canvas在上面绘制的,最后把这个bitmap返回出去,因为是枚举类,所以他们各自的逻辑是不会干预的,所以每次画布和画布的控制就省了很多代码,这也算一个小技巧吧,用来枚举类我们可以解决很多彼此相似又不是同一个实体的问题,所以大家一定要学好它。ok,到这里我们将的也差不多了,如果还有不懂的地方,欢迎大家留言。

扫描关注我的微信公众号:



写在最后

小组件这个功能平时使用的也不是很多,google对齐的设计也是烂的一比,各种限制各种加载出错,自由度非常的差,所以往往市面上面的小组件也不是特别的酷炫,要是非要自己去定制的话目前应该只有这个教程可以帮你完美的完成,文章前前后后写了一个多月,中间断了很久,本来不打算写的了,事情拖得越久越不想去做,但是前段时间发现我还没有写完的文章居然被编辑推荐到首页了,激动的左摇右晃呀,于是乎拼命的提醒自己一定要完成一定要完成,现在终于告一段落了,写了这么多篇文章,这篇算是最长最用心的了,以后我会写好每篇文章,最求的不再是数量而是质量。目前移动行业这么火,门槛又不是很高,培训出来的人一大堆,导致很多人找不到工作,开发出来的软件质量也是烂的一B,真是无力吐槽~~我们要学的东西还很多,比起大学里也想我们确实很了不起了,比起你心目中的自己,你是非常的优秀了,能照着百度敲代码上班,能月薪过万,很是得意洋洋,这也就是为什么软件会有那么多莫名其妙的crash的原因。开发软件就像使用电脑一样,易懂难精,要开发优质的软件,写出精炼的代码,需要非常深的功力,需要对android底层非常的熟悉。安卓是一个系统,要学透不是那么简单的,所以平时希望大家静下心来,扎实的学,不要成为别人口中的程序员,工程师才是你们的目标。好来,最后来说下今天的主题吧,小组件是利用远程广播来更新视图,内容载体是remoteview,实时刷新需要我们自己开一个服务动态的发送广播更新,利用imageview中的bitmap我们实现了自定义view,利用自定义view我们就可以生成绚丽多彩的画面,实现我们的需求。好啦,谢谢大家能耐心的看到这里~~完








  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
TabLayout是Android中常用的一个选项卡控件,可以方便地实现多页切换效果。TabLayout提供了默认的样式,但是我们也可以自定义TabLayout的样式,包括自定义选项卡的布局和样式。下面介绍一下如何自定义TabLayout的布局和样式。 1. 自定义选项卡的布局 我们可以通过TabLayout的setCustomView()方法来设置自定义的选项卡布局。例如,我们可以在布局文件中定义一个自定义的选项卡布局: ``` <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/tab_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_marginTop="8dp" android:src="@drawable/ic_launcher"/> <TextView android:id="@+id/tab_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/tab_icon" android:layout_centerHorizontal="true" android:text="Tab" android:textSize="12sp"/> </RelativeLayout> ``` 然后在代码中使用setCustomView()方法来设置自定义的选项卡布局: ``` TabLayout.Tab tab = tabLayout.newTab(); tab.setCustomView(R.layout.custom_tab_layout); tabLayout.addTab(tab); ``` 2. 自定义选项卡的样式 我们也可以通过TabLayout的style属性来自定义选项卡的样式。例如,我们可以在styles.xml文件中定义一个新的样式: ``` <style name="CustomTabLayout" parent="Widget.Design.TabLayout"> <item name="tabIndicatorColor">@color/colorAccent</item> <item name="tabTextAppearance">@style/CustomTabTextAppearance</item> </style> <style name="CustomTabTextAppearance" parent="TextAppearance.Design.Tab"> <item name="android:textSize">12sp</item> <item name="android:textColor">@color/colorPrimary</item> </style> ``` 然后在布局文件中使用该样式: ``` <android.support.design.widget.TabLayout android:id="@+id/tabLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorWhite" app:tabMode="scrollable" app:tabGravity="fill" style="@style/CustomTabLayout"/> ``` 这样就可以自定义选项卡的样式了。 总结 通过自定义选项卡的布局和样式,我们可以灵活地定制TabLayout的样式,让它更符合我们的需求。同时,我们也可以通过自定义选项卡的布局来实现更加复杂的选项卡效果,例如带有图片和文字的选项卡。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值