一、雞湯
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,需要在清單文件中注冊標簽。
AppWidgetProviderInfo:該類只需寫xml文件就可以了,xml放在res/xml下面,跟標簽為.
RemoteViewsService:該類繼承自service,需要在清單文件中注冊。
RemoteViewsFactory:該類為RemoteViewsService的內部類,處理service中的工作。
ok,有了上面的大體介紹,下面我們就這兩種情況具體來介紹一下。
只含有textview、button等情況
首先我們要準備一個appwidget用來顯示的布局文件,這里我們假定只有一個button,放在layout下面就ok,取名為widget_layout,具體代碼就補貼出來了,你自己決定寫什么樣的效果,不過只能含有我上面說的哪幾種情況哦。接下來我們需要準備一個AppWidgetProviderInfo類,上面知道這個類是在res/xml下面新建一個xml文件來自動生成的,具體代碼如下類似:
android:initialLayout="@layout/widget_layout"
android:minHeight="300dp"
android:minResizeHeight="90dp"
android:minResizeWidth="190dp"
android:resizeMode="horizontal|vertical"
android:minWidth="200dp">
簡要的介紹下屬性值:
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,所以我們要在清單文件中配置標簽,如下所示:
android:name="android.appwidget.provider"
android:resource="@xml/widget_provider">
這里的是必須要寫的,名字是固定的,resource就是我們創建的上面的那個xml文件的位置。其中
這個是必須要寫的一個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,如下所示:
android:id="@+id/kk"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:id="@+id/widget_listview"
android:background="#000"
android:layout_width="match_parent"
android:layout_height="match_parent">
OK,前面我們說到,要使用這種帶有子布局的情況需要使用remoteviewsService和RemoteViewsFactory,那么我們新建一個類叫RemoteviewsServiceImp,讓它繼承與RemoteviewsService,然后實現里面的方法,不要忘記在manifest文件里面添加標簽,標簽內的內容如下:
android:name=".RemoteViewServiceImp"
android:permission="android.permission.BIND_REMOTEVIEWS">
**********注意權限哦!
因為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的布局文件,里面的代碼如下:
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
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" />
下面我們來到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 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 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我們就可以生成絢麗多彩的畫面,實現我們的需求。好啦,謝謝大家能耐心的看到這里~~完