1. 程式人生 > >AppWidget實現自定義view

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我們就可以生成絢麗多彩的畫面,實現我們的需求。好啦,謝謝大家能耐心的看到這裡~~完

相關推薦

AppWidget實現定義view

一、雞湯 appwidget是android中小元件,我們經常說的widget其實是指的那些button、textview、imageview等這些小控制元件,而appwidget則是嵌入到別的app中的activity中顯示的一種檢視。通常我們的appwidget都是嵌入

實現定義view(2):仿Android QQ多螢幕顯示ListView的效果

本文在《仿 UC,墨跡天氣左右拖動 多螢幕顯示效果》的基礎上對程式碼進行修改,模仿Android QQ主介面的分屏ListView滑動效果。 當進行橫向滑動時,會切換螢幕,當縱向滑動時,ListView會滾動。 效果圖如下: 程式碼如下: FlingGallery.

一步步實現定義View之雷達圖

之前在專案中需要用到雷達圖,我就在github上挑了一個用於專案中實現了需求。但是作為一隻有追求的程式猿,我還是想自己實現一下,忙裡偷閒地實現了一個雷達圖。下面看一下效果圖吧: 接著詳細地介紹一下我的實現思路吧 1.繪製背景圖 首先這裡需要注意的一

簡單實現定義View隨手指拖動

1:自定義一個類繼承View; private float x=100; private float y=100; private Paint paint; 2:重寫三到四個構造方法 3:在構造方法中初始化筆 public CircleView(Context con

iOS實現定義View的屬性在interface builder裡面設定

自定義View的屬性要實現像系統的view那樣在Attribute Inspector設定然後在interface builder檢視效果,需要用到兩個關鍵字IBInspectable和IB_DESIGNABLE。 0> 通過User Defined Rumtime

Android -- 定義view實現keep歡迎頁倒計時效果

super onfinish -m use new getc awt ttr alt 1,最近打開keep的app的時候,發現它的歡迎頁面的倒計時效果還不錯,所以打算自己來寫寫,然後就有了這篇文章。 2,還是老規矩,先看一下我們今天實現的效果   相較於我們常見的倒計時

Android定義View——實現水波紋效果類似剩余流量球

string 三個點 pre ber block span 初始化 move 理解 最近突然手癢就想搞個貝塞爾曲線做個水波紋效果玩玩,終於功夫不負有心人最後實現了想要的效果,一起來看下吧: 效果圖鎮樓 一:先一步一步來分解一下實現的過程 需要繪制一個正弦曲線(sin

Android從零擼美團(三) - Android多標籤tab滑動切換 - 定義View快速實現高度定製封裝

這是【從零擼美團】系列文章第三篇 【從零擼美團】是一個高仿美團的開源專案,旨在鞏固 Android 相關知識的同時,幫助到有需要的小夥伴。 GitHub 原始碼地址:github.com/cachecats/L… Android從零擼美團(一) - 統一管理 Gradle 依賴 提取到單獨檔案中 Andr

定義View之指南針(反編譯別人的程式碼實現

一、說明        偶爾點開魅族手機內建的工具箱應用,發現其指南針做的還不錯,就想模擬做一個類似的效果,在這裡我們不準備自己從頭開始編寫程式碼,而是採用一點黑科技,首先,我們從魅族系統中匯出工具箱應用的apk,然後反編譯apk,結合

定義view實現圓中心顯示文字

自定義view實現:畫一個矩形 然後畫一個圓 再在圓中心顯示文字,效果如下 RectF rect = new RectF(100,100,500,500);//畫一個矩形 Paint mPaint = new Paint(); mPaint.setColor(

Android 定義View(繼承原生元件)實現拖動移位效果

自定義View實現拖拽移位效果 通過繼承GridLayout實現的拖拽移位效果 首先建立Class類繼承GridLayout並重寫前三個構造方法 public class MyGridlayout extends GridLayout implement

Android 定義View-----流式佈局(粗糙實現)

//首先檢視一下佈局介面吧 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app

定義view實現抽獎轉盤

------>自定義view類 public class LotteryView extends View implements View.OnClickListener {     private Paint mPaint;     private

Android定義view實現圖片選色器

https://www.jb51.net/article/141336.htm 這篇文章主要為大家詳細介紹了Android自定義view實現圖片選色器,具有一定的參考價值,感興趣的小夥伴們可以參考一下 簡介 本文介紹該自定義view的使用及實現的方法,主要實現以下幾個功能: - 選取

定義View實現圓形進度條跳轉頁面

效果: //首先在values資料夾下建立一個attrs.xml: ?xml version=“1.0” encoding=“utf-8”?> //佈局: <?xml version="1.0" encoding="utf-8"?>

定義View實現五子棋遊戲

成功的路上一點也不擁擠,因為堅持的人太少了。 ---簡書上看到的一句話 1 2 未來請假三天順帶加上十一回家結婚,不得不說真是太坑了,去年婚假還有10天,今年

Android :定義view實現簡易的轉盤

直接先上效果圖 xml裡面的程式碼 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

定義View繼承現有的Toast,實現訂單提醒的Toast,從左下角顯示然後退出

自定義View繼承現有的Toast,實現訂單提醒的Toast,從左下角顯示然後退出 /** * Created by on 16-2-4. */ public class NotifyToast extends Toast { private long last

android定義View之仿通訊錄側邊欄滑動,實現A-Z字母檢索

我們的手機通訊錄一般都有這樣的效果,如下圖: OK,這種效果大家都見得多了,基本上所有的android手機通訊錄都有這樣的效果。那我們今天就來看看這個效果該怎麼實現。 一.概述 1.頁面功能分析 整體上來說,左邊是一個ListView,右邊是一個自定義View,但

Android 定義View實現拖拽效果

騰訊QQ有那種紅點拖動效果,今天就來實現一個簡單的自定義View拖動效果,再回到原處,並非完全仿QQ紅點拖動 先來看一下效果圖 簡單說一下實現步驟 1.建立一個類繼承View 2.繪製出一個