主頁 > 移動端開發 > 記錄學習Android基礎的心得09:常用控制元件(高級篇)

記錄學習Android基礎的心得09:常用控制元件(高級篇)

2021-11-09 08:25:17 移動端開發

文章目錄

  • 前言
  • 一、螢屏顯示
    • 1.顯示屏的硬體引數
    • 2.Android系統對螢屏引數的管理
  • 二、自定義控制元件
    • 1.分析控制元件
    • 2.宣告屬性
    • 3.構造物件
    • 4.測量尺寸
    • 5.定位坐標
    • 6.繪制控制元件
  • 三、頁面布局優化
    • 1.減少重復布局
    • 2.按需加載布局資源
    • 3.自定義主題
  • 四、自定義通知欄
    • 1.在通知欄顯示通知
    • 2.自定義通知欄的視圖
  • 五、碎片
    • 1.Fragment的生命周期
    • 2.Fragment的管理
    • 3.Fragment的使用
    • 4.Fragment與Activity通信
  • 總結


活著就要做有意義的事;有意義的事就是好好活著,–《士兵突擊》


前言

高級篇是系統總結常用控制元件系列四部曲的最后一章,內容包括:螢屏顯示,自定義控制元件,頁面布局優化,自定義通知欄,碎片,關于控制元件的更多知識可參考專業的工具書,當然,更高級的技巧也不像本系列文章的大白話一樣,肯定涉及到復雜的系統代碼和數學計算,學習起來也困難得多,好了,不多 BB,進入本文正題,

一、螢屏顯示

1.顯示屏的硬體引數

大部分人都知道,顯示屏是由像素陣列組成的,用英寸表示顯示屏的尺寸大小,用解析度表示顯示屏的成像質量,可能還有部分人對這些概念還不太了解,那么接下來就系統總結一下關于顯示屏的各個引數含義:
(1) 像素
顯示屏的像素指一個最小的發光單元,即便尺寸相同的顯示屏的像素長寬值也會由于解析度的不同而不同,如圖是 OLED顯示屏的像素結構:
在這里插入圖片描述
(2) 解析度
顯示屏的解析度指“行像素值 x 列像素值”,如小米6螢屏的解析度為1920x1080表示該顯示屏每一行有1080個像素,每一列有1920個像素,顯然,相同尺寸的顯示屏的解析度越大,那么它的發光單元越多,顯示的影像就越清晰,
(3) 色彩深度
色彩深度指顯示屏的一個像素發光的顏色有多少種,一般用“位”(bit)來表示,如單色屏的每個像素有亮或滅兩種狀態(即2種顏色),那么用1個資料位就可以表示該像素的所有狀態,所以它的色彩深度為1bit,其它常見的顯示屏色深為16bit、24bit,
(4) 顯示屏尺寸
顯示屏的大小一般以英寸(1英寸=2.54厘米)表示,這個長度是指螢屏對角線的長度,通過螢屏的對角線長度及長寬比即可確定螢屏的實際長寬尺寸,
(5) 點距
點距指兩個相鄰像素之間的距離,它會影響畫質的細膩度及觀看距離,點距越小,畫質越細膩,如LED點陣顯示屏的點距一般都比較大,所以適合遠距離觀看,

2.Android系統對螢屏引數的管理

(1) Android的尺寸單位
獲取手機螢屏的尺寸資訊需使用 DisplayMetrics,它的常用屬性有:
①heightPixels:計算螢屏的高度值(以像素px為單位),
②widthPixels:計算螢屏的寬度值(以像素px為單位),
③density:像素密度,表示1dp單位包含多少個px單位,
比如,獲取螢屏的寬度(像素點數)可通過以下方式(其他屬性的獲取同理):

    // 獲得螢屏的寬度
    public int getScreenWidth(Context ctx) {
        // 從系統服務中獲取視窗管理器
        WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        // 從視窗管理器中獲取顯示引數保存到dm物件中
        wm.getDefaultDisplay().getMetrics(dm);
        return dm.widthPixels; // 回傳螢屏的寬度
    }

上面提到的dp是大家在XML檔案中經常使用的,它是一種與具體螢屏解析度無關的尺寸單位,只與螢屏自身e的尺寸大小有關,尺寸相同,解析度不同的螢屏,以dp為單位計量的圖形最終顯示的尺寸相同,通常Android中類有關尺寸的方法采用的是px單位,而XML檔案使用的是dp單位,故有時需要使用DisplayMetrics的density屬性進行單位換算:當density=1,表示1dp=1px,density=1.5,表示2dp=3px,density=2,表示1dp=2px,具體代碼如下:

    // 從 dp 單位 轉成為 px(像素)
    public int dip2px(Context context, float dpValue) {
        // 獲取當前手機的像素密度
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f); // 四舍五入取整
    }

    // 從 px(像素) 單位 轉成為 dp
    public int px2dip(Context context, float pxValue) {
        // 獲取當前手機的像素密度
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f); // 四舍五入取整
    }

Android還支持的尺寸單位有:in(英寸),mm(毫米),pt(磅,1pt=1/72 in),sp(文字尺寸),其中sp是專門用于設定文字尺寸的單位,被設定成該單位的文字會隨著系統設定的字體大小而變大或變小(若使用其他單位設定字體大小,則不會隨系統設定變化而變化),
(2)Android像素的顏色
像素作為一個基本的發光單元,它可以顯示由不同光強的紅,綠,藍三原色混合而成的不同顏色,在Android中,顏色值由透明度AA,三原色RGB組成,有6位十六進制(RRGGBB),8位十六進制(AARRGGBB)兩種編碼,透明度AA的值為FF時,表示完全不透明,為00時表示完全透明,三原色數值(00-FF)越大,則對應的光成分占比越多,數值越小則占比越少,當三原色的數值都相等但不為最大值或最小值時,會變成灰色光,
在XML檔案中使用十六進制的顏色值需要添加前綴"#",即"#(AA)RRGGBB",在代碼中直接使用顏色數值需要注意:只能使用8位的顏色編碼,6位的十六進制顏色默認是完全透明的,故相當于沒有效果,

二、自定義控制元件

當Android提供的原生UI控制元件不能滿足使用需求時,開發者往往需要自定義控制元件,比如,上一篇文章中自定義了一個能同時繪制矩形和圓形的控制元件(雖然沒卵用),個人覺得自定義控制元件涉及到的知識應該是Android基礎知識中最難的一部分了,其次是四大組件中的ContentProvider,其實我自己對于自定義控制元件也不是很熟練,

自定義控制元件通常分為兩種情況:(1)基于現有的控制元件,只優化部分外觀和功能,優化后的控制元件保留著原有控制元件的大部分特征,比如前文使用過的翻頁標題欄 PagerTabStrip不支持在XML檔案中設定標題的文字樣式,那么完全可以繼承PagerTabStrip類,在它的基礎上添加一些方法來支持XML檔案中的文字樣式設定,
(2)基于View或者ViewGroup,完全由開發者自己繪制控制元件的外觀,處理控制元件的回呼事件,這種自定義控制元件往往有特殊的外觀和功能,比如,顯示信號的示波器控制元件,控制方向的搖桿控制元件等根據需求定制的控制元件,

按照我自己的理解,自定義視圖的流程通常分為六個步驟:分析控制元件,宣告屬性,構造物件,測量尺寸,定位坐標,繪制控制元件, 接下來自定義一個搖桿控制元件的例子來熟悉一下自定義控制元件的流程,先看一下效果動圖(下圖可能有點模糊,這是由于我先錄屏然后再轉成gif格式的圖片,,):

在這里插入圖片描述

1.分析控制元件

(1)外觀分析
肉眼望去,本例子中搖桿外觀可分為四個部分組成:搖桿的正方形底盤(方向背景貼圖),搖桿的桿,搖桿的球,和球所處的高亮扇形區域,本搖桿控制元件以正方形為邊界,在控制元件里面繪制了方向背景貼圖,搖桿的中間是一個可以拽動的小球,當手指觸摸滑動搖桿控制元件所處的區域時,小球會追蹤手指軌跡,并在正方形的內切圓邊上移動,小球的圓心和控制元件的中心還會繪制一條一定寬度的線段(搖桿的桿),同時,會高亮顯示小球所處的區域,可以看到,這兩個搖桿控制元件的四個組成部分都不相同,表示此控制元件的這四個部分是可以自定義的,那么這里先自己繪制兩個(丑陋的)方向背景貼圖:
在這里插入圖片描述
(2)功能分析
最基本的功能是作為一個實時跟蹤手指移動軌跡的自定義控制元件,其次外界可以獲取本控制元件的高亮區域,
顯然,這種外觀由幾個簡單圖形組成,功能單一的自定義控制元件,繼承View來開發就完全可以了,那么同時定義搖桿控制元件的類名就叫RockerView吧(和系統UI控制元件采用相同風格命名),

2.宣告屬性

控制元件的自定義屬性大多與控制元件的外觀有關,宣告屬性有兩個方面:①在XML屬性資源檔案中宣告屬性,②在自定義控制元件類中宣告屬性,
一般來說,XML檔案中自定義的屬性在類中都要有一一對應的變數,此外,在自定義的控制元件類中還需要一些其他的屬性,這樣,我們不僅可以在XML布局檔案中創建該控制元件,也能在Java代碼中創建,
由于是繼承自View,故View中有用的通用屬性我們不需要重新宣告,我們只需抽離出自定義控制元件的特有屬性即可,比如,搖桿控制元件的尺寸大小完全可用View的layout_xxx屬性設定,但特有屬性:搖桿的方向背景貼圖,搖桿的桿的粗細,桿的顏色,搖桿的球的大小,球的顏色,控制元件均分的扇形區域數量,扇形區域的高亮顏色需要我們自定義(本例只做演示功能,后來者可以基于實際情況定義更多的屬性,讓搖桿有更漂亮的外觀),
(1)在XML屬性資源檔案中宣告屬性
首先,在res/values目錄下新建一個attrs.xml的屬性資源檔案,指定檔案的根標簽為resources,resources標簽可以添加兩個子標簽:
①attr:宣告控制元件的一個屬性,attr標簽可以指定name表示屬性的名稱,format表示屬性的值的格式(資料型別),
②declare-styleable:定義一個styleable物件,它是一組attr標簽的集合,用于組合多個屬性,此標簽的name屬性通常設定為自定義控制元件的類名,
那么從以上的控制元件分析很容易得到搖桿控制元件的屬性如下:

<resources>
    <declare-styleable name="RockerView">
        <attr name="rocker_bar_color" format="color" /><!-->搖桿的桿的顏色<-->
        <attr name="rocker_bar_width" format="integer" /><!-->搖桿的桿的寬度<-->
        <attr name="rocker_ball_color" format="color" /><!-->搖桿的球的顏色<-->
        <attr name="rocker_ball_radius" format="integer" /><!-->搖桿的球的半徑<-->
        <attr name="rocker_plate_background" format="reference" /><!-->搖桿的底盤的背景貼圖<-->
        <attr name="rocker_sector_num" format="integer" /><!-->搖桿的底盤平均分為多少個扇形區域<-->
        <attr name="rocker_sector_color" format="color" /><!-->搖桿的扇形區域的顏色<-->
    </declare-styleable>
</resources>

在自定義好屬性檔案之后,怎么使用屬性資源檔案所定義的屬性,取決于自定義控制元件類的方法實作,即如何從布局檔案中獲取控制元件的自定義屬性的值呢?答案是可以在自定義控制元件的構造方法中通過它的引數 AttributeSet獲取在XML布局檔案中設定的這些屬性值,
(2)在自定義控制元件類中宣告屬性
要在自定義控制元件類中創建與自定義的屬性一一對應的變數,并添加一些額外的必要屬性變數,如搖桿控制元件中屬性變數如下:

public class RockerView extends View {
    private Context mContext; // 宣告一個背景關系物件
    //!!搖桿的背景貼圖,扇形區域,桿,球的各種屬性,如果在XML檔案中未指定這些屬性,則使用以下默認值:
    private Bitmap rockerPlate; // 搖桿的底盤的背景貼圖
    private Region[] sectorRegions;//保存搖桿均分的每個區域
    private int rockerSectorNum = 8;//搖桿的底盤默認平均分為8個扇形區域
    private int rockerSectorColor = Color.CYAN;// 搖桿的球所落在區域的顏色
    private int rockerBarColor = Color.GREEN; // 搖桿的桿的顏色
    private int rockerBarWidth = 30; // 搖桿的桿的寬度
    private int rockerBallColor = Color.RED; // 搖桿的球的顏色
    private int rockerBallRadius = 50; // 搖桿的球的半徑(即小圓的半徑:r )
    //!!
    private Matrix rockerPlateMatrix = new Matrix();//此矩陣用于背景貼圖的縮放變換以填滿本視圖
    private Paint rockerPlatePaint = new Paint();//繪制背景貼圖的畫筆
    private Paint rockerSectorPaint = new Paint();//繪制扇形區域的畫筆
    private Paint rockerBarPaint = new Paint();//繪制搖桿的桿的畫筆
    private Paint rockerBallPaint = new Paint();//繪制搖桿的球的畫筆

}

3.構造物件

在大腦中構想好控制元件的外觀和功能后,需要在類中通過方法實作出來,首先重寫構造方法獲取控制元件的屬性值和初始化控制元件的各種變數,開發者一般重寫三個不同引數的構造方法:
①只帶一個引數(Context)的方法,此方法在從代碼中生成控制元件時被呼叫,
②帶兩個引數(Context,AttributeSet)的方法,此方法在從XML布局檔案中生成控制元件時被呼叫,引數AttributeSet是從XML布局檔案中獲取的該控制元件已經設定好的屬性集合,
③帶三個引數(Context,AttributeSet,int)的方法,在方法②的基礎上,并且還要從代碼中指定默認的風格生成控制元件時,一般可以不重寫該方法,

要獲取控制元件已經設定好的屬性的值,需要用到Context的方法先獲取TypedArray物件:
public final TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs):第一個引數是在XML布局檔案設定的該控制元件的所有屬性集(AttributeSet),第二個引數表示描述該控制元件自定義屬性的檔案ID(R.styleable.xxx,即第二步中自定義的屬性檔案),
從布局檔案中獲取屬性陣列 TypedArray后,然后用該物件的getxxx方法獲取各種屬性的值,最后回收屬性陣列,
TypedArray的getxxx方法用于獲取屬性集中指定屬性名稱的值,第一個引數為R.styleable.屬性檔案名_屬性名,這種命名方式是Android SDK自動生成的,開發者不必奇怪,第二個引數是指定屬性為空時使用的默認值,
不同資料型別的屬性值對應的獲取方法如下:
boolean:布爾,獲取方法為getBoolean;
integer:整型,獲取方法為getInteger;
float:小數,獲取方法為getFloat;
string:字串,獲取方法為getString;
eum:列舉值,獲取方法為getInt;
flag:標志位,獲取方法為getInt;
color:顏色值,取值為開頭帶#的6或8位的十六進制數,獲取方法為getColor;
dimension:尺寸,取值為末尾帶尺寸單位的值,獲取方法為getDimension;
fraction:百分數,取值為末尾帶%的數,獲取方法為getFraction;
reference:資源目錄下的檔案參考,獲取此ID的方法為getResourceId,

獲取控制元件的屬性值之后,接著初始化控制元件的各種變數,那么寫出自定義的搖桿控制元件的構造方法如下:

	public RockerView(Context context) {
        super(context);
    }

    //在含有兩個引數的建構式中獲取XML檔案中設定該控制元件的屬性值
    public RockerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        if (attrs != null) {
            // 根據RockerView的屬性定義,從布局檔案中獲取屬性陣列
            TypedArray attrArray = mContext.obtainStyledAttributes(attrs, R.styleable.RockerView);
            // 獲取布局檔案中的搖桿的底盤的背景貼圖
            rockerPlate = BitmapFactory.decodeResource(mContext.getResources(),
                    attrArray.getResourceId(R.styleable.RockerView_rocker_plate_background, R.drawable.rocker_plate_background1));
            // 獲取布局檔案中的搖桿平均劃分的扇形區域
            rockerSectorNum = attrArray.getInteger(R.styleable.RockerView_rocker_sector_num, rockerSectorNum);
            // 獲取布局檔案中的搖桿的扇形區域的顏色
            rockerSectorColor = attrArray.getColor(R.styleable.RockerView_rocker_sector_color, rockerSectorColor);
            // 獲取布局檔案中的搖桿的桿的顏色
            rockerBarColor = attrArray.getColor(R.styleable.RockerView_rocker_bar_color, rockerBarColor);
            // 獲取布局檔案中的搖桿的桿的寬度
            rockerBarWidth = attrArray.getInteger(R.styleable.RockerView_rocker_bar_width, rockerBarWidth);
            // 獲取布局檔案中的搖桿的球的顏色
            rockerBallColor = attrArray.getInteger(R.styleable.RockerView_rocker_ball_color, rockerBallColor);
            // 獲取布局檔案中的搖桿的球的半徑
            rockerBallRadius = attrArray.getInteger(R.styleable.RockerView_rocker_ball_radius, rockerBallRadius);

            // 回收屬性陣列
            attrArray.recycle();
        }
        //根據從XML總獲取的屬性值設定相關畫筆,一般繪制圖片的畫筆不需要特別的設定引數
        rockerBarPaint.setAntiAlias(true); // 設定畫筆為無鋸齒
        rockerBarPaint.setDither(true); // 設定畫筆為防抖動
        rockerBarPaint.setColor(rockerBarColor); // 設定畫筆的顏色
        rockerBarPaint.setStrokeWidth(rockerBarWidth); // 設定畫筆的線寬
        rockerBarPaint.setStrokeCap(Paint.Cap.ROUND); //設定線段的端點形狀
        rockerBarPaint.setStyle(Paint.Style.FILL); // 設定畫筆的型別:STROKE表示空心,FILL表示實心
        rockerBallPaint.setAntiAlias(true);
        rockerBallPaint.setDither(true);
        rockerBallPaint.setColor(rockerBallColor);
        rockerBallPaint.setStyle(Paint.Style.FILL);
        rockerSectorPaint.setColor(rockerSectorColor);
        rockerSectorPaint.setStyle(Paint.Style.FILL);
    }

注意: 在XML布局檔案中使用自定義控制元件時,需要在布局檔案的根標簽中添加命名空間的宣告:xmlns:app="http://schemas.android.com/apk/res-auto"
這里xmlns:后面的app為命名空間的簡短別名前綴,開發者可以自定義該名稱,在布局檔案中添加自定義控制元件時,必須使用該控制元件的全路徑名稱(再說一遍,開發者只需輸入關鍵字母,AS會彈出備選框供我們選擇,十分方便),

要實作動圖中的布局效果,頁面布局檔案的代碼如下:

<FrameLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	
	xmlns:app="http://schemas.android.com/apk/res-auto"
	
	android:layout_width="match_parent"
	android:layout_height="match_parent">
	<com.example.myapplication.widget.RockerView
		android:id="@+id/rockerView1"
		android:layout_width="400px"
		android:layout_height="400px"
		android:layout_marginLeft="50px"
		android:layout_marginTop="50px"
		app:rocker_ball_color="@color/red"
		app:rocker_ball_radius="40"
		app:rocker_bar_color="@color/green"
		app:rocker_bar_width="30"
		app:rocker_sector_num="8"
		app:rocker_sector_color="@color/white"
		app:rocker_plate_background="@drawable/rocker_plate_background1">
	</com.example.myapplication.widget.RockerView>
	<com.example.myapplication.widget.RockerView
		android:id="@+id/rockerView2"
		android:layout_width="500px"
		android:layout_height="500px"
		android:layout_marginLeft="50px"
		android:layout_marginTop="1000px"
		app:rocker_ball_color="@color/blue"
		app:rocker_ball_radius="60"
		app:rocker_bar_color="@color/purple"
		app:rocker_bar_width="40"
		app:rocker_sector_num="16"
		app:rocker_sector_color="@color/black"
		app:rocker_plate_background="@drawable/rocker_plate_background2">
	</com.example.myapplication.widget.RockerView>
</FrameLayout>

4.測量尺寸

重寫onMeasure測量方法,計算控制元件的寬和高,眾所周知,在布局檔案中對控制元件的寬和高有三種賦值方式:match_parent,wrap_content和具體帶單位的尺寸值,在Java代碼中分別對應布局引數 ViewGroup.LayoutParams的MATCH_PARENT,WRAP_CONTENT和具體整型數值,其中控制元件如果被設定為match_parent和具體的數值的話,都很容易計算出控制元件的尺寸:被設定為具體帶單位的尺寸值的話,就直接獲取該值就行了,被設定為match_parent時,就是與父控制元件的尺寸相同,當前控制元件就不需要計算自己的尺寸了,至于wrap_content的情況則需要開發者自己計算本控制元件的尺寸,
一般來說,自定義控制元件的內部中的主要內容有三類:文字,圖片,子控制元件,不同內容的測量方式如下:
(1)文字
文字的寬度使用Paint類的measureText方法測得,至于文字的高度則稍微復雜點,大家都知道我們在學習寫英文字母的時候,使用的是四線格來練習的:
在這里插入圖片描述
四線格從上往下的第三條線稱作基線,使用四線格可以規范字母的位置,比如確定了一行文字基線的位置,那么該行文字的位置也就確定了,而在Android中對文字的定位正是以基線為參考線的(比如使用Canvas的drawText方法繪制文字時,其引數Y坐標就是基線在螢屏的Y坐標),如圖:
在這里插入圖片描述
除了基線外,還有四條輔助線:
top: 文字所在行的最高高度所在線,
ascent: 單個文字的最高高度所在線,
descent:單個文字的的最低高度所在線,
bottom: 文字所在行的最低高度所在線,

字體尺寸 Paint.FontMetrics提供了與這幾條線相關的屬性:
top,行頂與基線的距離,
ascent,字符頂與基線的距離,
descent,字符低與基線的距離,
bottom,行低與基線的距離,
leading,行間距,
注意,top,ascent,descent,bottom的值都是相對于基線的距離而得到的,并不是在螢屏坐標系下的Y坐標的值,即只有確定了基線的Y坐標,這四條輔助線的Y坐標才能確定,

那么要得到文字自身的高度,可用descent減去ascent,要得到文字所在行的高度,可用bottom減去top,再加上leading,測量文本尺寸的代碼如下:

 // 獲取文本的寬度
    public float getTextWidth(String text, float textSize) {
        if (TextUtils.isEmpty(text)) {
            return 0;
        }
        Paint paint = new Paint(); // 創建一個畫筆物件
        paint.setTextSize(textSize); // 設定畫筆的文本大小
        return paint.measureText(text); // 利用畫筆丈量指定文本的寬度
    }
    // 獲取文本的高度
    public float getTextHeight(String text, float textSize) {
        Paint paint = new Paint(); // 創建一個畫筆物件
        paint.setTextSize(textSize); // 設定畫筆的文本大小
        FontMetrics fm = paint.getFontMetrics(); // 獲取畫筆默認字體的度量衡
        return fm.descent - fm.ascent; // 回傳文本自身的高度
        //return fm.bottom - fm.top + fm.leading;  // 回傳文本所在行的行高
    }

(2)圖形尺寸的測量:若圖形是用位圖物件Bitmap表示的,可通過位圖物件的getWidth和getHeight方法獲取寬高,若圖形是Drawable物件表示的,則可通過它的getIntrinsicWidth和getIntrinsicHeight方法獲取寬高,

(3)子控制元件的測量
自定義控制元件中可能含有許多其他的子控制元件,如果一個個去測量這些子控制元件的尺寸的話,那開發者就太難受了,好在View默認提供了一種對所有子控制元件的測量思路:實作了在整個控制元件樹中從父控制元件向子控制元件的遍歷測量,每個控制元件在遍歷程序中將自身的尺寸資訊保存起來,然后向下傳遞,這樣遍歷一次整個控制元件樹,就得到了所有控制元件的尺寸資訊,
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 是該測量思路的主要實作方法,它的兩個引數是從父控制元件傳過來的值,是父控制元件想讓子控制元件的寬高滿足的建議值,這種值由mode + size兩部分組成:
①mode的獲取通過MeasureSpec的getMode方法獲取,mode的取值有三種:
MeasureSpec.UNSPECIFIED:對應在XML布局檔案中將該控制元件的尺寸設定為wrap_content的情況,這時父控制元件沒有辦法給出適當的建議尺寸值,故需要開發者自己計算本控制元件的尺寸,
MeasureSpec.EXACTLY:父控制元件給出具體的建議尺寸數值,
MeasureSpec.AT_MOST:父控制元件給出當前控制元件可以被設定的最大寬高,
②size的獲取通過MeasureSpec的getSize方法,當mode取值為EXACTLY或者AT_MOST時,可得到具體的size,
當開發者在XML布局檔案中將控制元件尺寸設定好之后,然后在onMeasure中計算好控制元件的寬和高之后,還需要在onMeasure方法中最后呼叫setMeasuredDimension方法設定控制元件最終的尺寸,

本例中搖桿控制元件尺寸的測量程序如下:

    private final int ROCKER_VIEW_SIZE = 400;//本視圖默認的尺寸大小

    //通過onMeasure函式測量本視圖大小
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         //獲取上級視圖給出的關于本視圖大小的測量模式
         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
         int rockerViewMinSize;//本視圖占據的區域是一個正方形,故長寬應該一致
         if (widthMode == MeasureSpec.UNSPECIFIED ||
                 heightMode == MeasureSpec.UNSPECIFIED){//上級視圖未指定本視圖的尺寸大小,則使用默認的尺寸
             rockerViewMinSize = ROCKER_VIEW_SIZE;
         }else {//若上級視圖給出本視圖確定的寬高尺寸,那么取寬高中最小值作為本視圖的區域
             rockerViewMinSize = Math.min(MeasureSpec.getSize(widthMeasureSpec),
                     MeasureSpec.getSize(heightMeasureSpec));
         }
         //向上級視圖確認提交本視圖的尺寸大小
         setMeasuredDimension(rockerViewMinSize,rockerViewMinSize);
    }

5.定位坐標

測量好控制元件尺寸后,就可以在螢屏上找個地方把控制元件放下了,但問題是:放哪?下面這個抽象方法可以告訴你答案,
protected void onLayout(boolean changed, int left, int top, int right, int bottom) :可以讓控制元件按指定的規則在螢屏上布局,引數 left,top,right,bottom分別表示:本控制元件距離父控制元件的左,上,右,下邊的位置,
這里涉及到一個小知識:螢屏的坐標系和控制元件自身的坐標系,眾所周知,螢屏的坐標系是以左上頂點作為坐標原點,向右為X的正方向,向下為Y的正方向,控制元件占據一個矩形區域,在這個區域內可以自由繪制控制元件的外觀,控制元件的坐標系和螢屏的坐標系大體相似,即以該控制元件占據的區域的左上頂點作為坐標原點,向控制元件右方為X的正方向,向控制元件下方為Y的正方向,
在這里插入圖片描述
顯然,當控制元件恰好占據整個螢屏區域,那么兩者的坐標系就是相同的,不過當子控制元件被嵌套進父控制元件里面,那么它就會使用基于父控制元件的坐標系來布局,比如我們自定義控制元件里重寫onLayout方法時,如果有子控制元件的時候,那么就必須注意子控制元件的布局是按照當前控制元件的坐標系來定位的,而不是根據螢屏的坐標系,

當然,第四步和第五步是在自定義控制元件中有子控制元件的情況下才會變得困難的,本例中的搖桿控制元件直接繼承自View,而且沒有任何子控制元件,故也不需要考慮子控制元件導致的尺寸和坐標問題,那么搖桿控制元件的onLayout方法就可以不加修改,如下:

//通過onLayout函式確定本視圖在螢屏的位置坐標
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
}

那大家可能有疑惑,不是在第一步中分析控制元件外觀的時候,把一個搖桿控制元件分為了:搖桿的方向背景貼圖,搖桿的桿,搖桿的球,和搖桿的球所處的扇形區域這四個部分嗎?這些既然不算子控制元件,那么該如何計算這些組成部分的尺寸和位置坐標呢?

其實當本控制元件在螢屏的尺寸和位置坐標確定之后,首先,我們知道方向背景貼圖始終填滿本控制元件,故它的尺寸和位置與控制元件相同,其次,搖桿的桿是連接搖桿的球和控制元件中心的一條線段,故確定了球的位置也就確定了桿的位置,搖桿的球所處的扇形區域是隨著搖桿的球位置變化而變化的,同樣的,確定了球的位置也就確定了該區域的位置,
那么,綜上,只需要求出搖桿的球的位置坐標就行了,而球的坐標是隨著手指在本控制元件的觸摸位置而變化的,手指在本控制元件的觸摸位置可通過重寫onTouchEvent方法來獲得,那么,已知手指觸摸位置,怎么求搖桿球的位置呢?

這其實是一個簡單的數學問題,經過一定的抽象:將本控制元件占據的正方形區域的內切圓稱為大圓,其圓心為點a(Xa,Ya),將手指按下位置稱為點b(Xb,Yb),將搖桿的球占據的區域稱為小圓,其圓心為c(Xc,Yc),將小圓的圓心的移動軌跡稱為中圓,它和大圓是同心圓,示意圖如下:
在這里插入圖片描述
那么,該數學問題的描述和解如下:
在這里插入圖片描述
由上述的解題方法很容易編碼,計算小圓圓心軌跡如下:

    //手指按壓點b的坐標
    private float pressPointX;
    private float pressPointY;
    //ab連線所在的直線與中圓的交點,即小圓的圓心c的坐標
    private float smallCirclePointX;
    private float smallCirclePointY;

    //計算小圓的圓心坐標
    private void calSmallCirclePosition() {
        //ab所在的直線穿過大圓,必有兩個交點c1,c2
        //交點c1的坐標
        float interPointX1;
        float interPointY1;
        //交點c2的坐標
        float interPointX2;
        float interPointY2;
        //Xc1,2 =Xa±R/√(1+((Ya-Yb)/(Xa-Xb))^2)
        interPointX1 = (float) (bigCirclePointX + (middleCircleRadius / Math.sqrt(1 + Math.pow(((bigCirclePointY - pressPointY) / (bigCirclePointX - pressPointX)), 2))));
        interPointX2 = (float) (bigCirclePointX - (middleCircleRadius / Math.sqrt(1 + Math.pow(((bigCirclePointY - pressPointY) / (bigCirclePointX - pressPointX)), 2))));
        //Yc1,2 =Ya±R/√(1+((Ya-Yb)/(Xa-Xb))^2)*((Ya-Yb))
        interPointY1 = (float) (bigCirclePointY + (middleCircleRadius / Math.sqrt(1 + Math.pow(((bigCirclePointY - pressPointY) / (bigCirclePointX - pressPointX)), 2)) / (bigCirclePointX - pressPointX) * (bigCirclePointY - pressPointY)));
        interPointY2 = (float) (bigCirclePointY - (middleCircleRadius / Math.sqrt(1 + Math.pow(((bigCirclePointY - pressPointY) / (bigCirclePointX - pressPointX)), 2)) / (bigCirclePointX - pressPointX) * (bigCirclePointY - pressPointY)));
        //bc1的長度,bc2的長度
        float bc1 = (float) Math.sqrt(Math.pow(pressPointX - interPointX1, 2) + Math.pow(pressPointY - interPointY1, 2));
        float bc2 = (float) Math.sqrt(Math.pow(pressPointX - interPointX2, 2) + Math.pow(pressPointY - interPointY2, 2));
        //選擇c1和c2中距離按壓點b最近的點
        smallCirclePointX = bc1 < bc2 ? interPointX1 : interPointX2;
        smallCirclePointY = (smallCirclePointX == interPointX1) ? interPointY1 : interPointY2;
    }

至于搖桿占據的正方形邊界,區域劃分,大圓圓心坐標等一些初始位置資訊,可通過onLayout方法中給的引數計算而來,那么將onLayout方法補充完整(好家伙,倒敘手法記筆記了屬于是),如下:

//使用RectF用來保存搖桿視圖(正方形區域)在螢屏坐標系下的位置(暫時沒用到該物件!)
    private RectF rockerViewRectF = new RectF();
    //正方形的內切圓的半徑和圓心:(X-Xa)^2 + (Y-Ya)^2 = R^2
    private float bigCircleRadius;
    private float bigCirclePointX;
    private float bigCirclePointY;
    //中圓(小圓的圓心軌跡)的半徑
    private float middleCircleRadius;

    //通過onLayout函式確定本視圖在螢屏的位置坐標
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        //獲取搖桿視圖在螢屏坐標系下占據的正方形區域的布局位置
        rockerViewRectF.set(left,top,right,bottom);
        //獲取大圓在本視圖的坐標系下的布局位置
        //大圓半徑為視圖邊長的一半
        bigCircleRadius = (float) Math.min(getMeasuredHeight(),getMeasuredWidth()) / 2;
        //圓心的X坐標為視圖的邊長的一半
        bigCirclePointX = bigCircleRadius;
        //圓心的Y坐標為為視圖的邊長的一半
        bigCirclePointY =  bigCircleRadius;
        /** 注意不要用以下方法求大圓的圓心坐標:
         * bigCirclePointY = top + bigCircleRadius;
         * bigCirclePointX = left + bigCircleRadius;
         *這是由于螢屏的坐標系和視圖的坐標系是分開的,這里只用到視圖的坐標系
         * 視圖的坐標系同樣以視圖左上角為原點,向右為X正方向,向下為Y正方向
         */
        //中圓和大圓是同心圓,且中圓半徑為R-r,中圓的方程:(X-Xa)^2 + (Y-Ya)^2 = (R-r)^2
        middleCircleRadius = bigCircleRadius - rockerBallRadius;

        //縮放背景貼圖來填滿本視圖
        rockerPlateMatrix.reset();
        float scaleX = (float) getMeasuredWidth() / rockerPlate.getWidth();
        float scaleY = (float) getMeasuredHeight() / rockerPlate.getHeight();
        rockerPlateMatrix.setScale(scaleX,scaleY);

        //計算并填充均分的每個扇形區域
        calSectorRegion();

        //開始繪制時,手指還沒有觸摸本視圖,所以搖桿的球默認位于本視圖中心
        onFingerUP();
    }

    //根據扇形區域數量計算的每個扇形區域的坐標資訊
    private void calSectorRegion(){
        sectorRegions = new Region[rockerSectorNum];
        Path[] sectorPaths = new Path[rockerSectorNum];//每個扇形區域的輪廓
        int sweepAngle = 360 / rockerSectorNum;//每個扇形的張開的角度
        for (int i = 0; i < sectorPaths.length; i++) {//勾勒扇形輪廓
            sectorPaths[i] = new Path();
            sectorPaths[i].addArc(0,0,bigCircleRadius * 2,bigCircleRadius * 2,
                    i * sweepAngle,sweepAngle);
            sectorPaths[i].lineTo(bigCirclePointX,bigCirclePointY);
            sectorPaths[i].close();
        }
        //將扇形輪廓通過一定的裁剪填充進對應的區域:clipRegion1是搖桿視圖占據的正方形區域,clipRegion2是圓心不可能在的區域
        Region clipRegion1 = new Region(0,0,(int) bigCircleRadius * 2,(int) bigCircleRadius * 2);
        Region clipRegion2 = new Region();
        //搖桿球的圓心不可能在此圓形區域內
        Path awayBallPath = new Path();
        awayBallPath.addCircle(bigCirclePointX,bigCirclePointX,bigCircleRadius - rockerBallRadius * 2, Path.Direction.CW);
        clipRegion2.setPath(awayBallPath,clipRegion1);
        for (int i = 0; i < sectorRegions.length; i++) {
            sectorRegions[i] = new Region();
            sectorRegions[i].setPath(sectorPaths[i],clipRegion1);
            //取兩區域的差
            sectorRegions[i].op(clipRegion2, Region.Op.DIFFERENCE);
        }
    }
    
    //重寫該函式,獲取手指在該視圖的觸摸資訊
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_MOVE){
            onFingerDown(event.getX(),event.getY());
        }else if (event.getAction() == MotionEvent.ACTION_UP){
            onFingerUP();
        }
        return true;
    }

    //當手指觸摸本視圖時,則重新計算小圓的圓心坐標,然后重新繪制本視圖
    private void onFingerDown(float X, float Y) {
        this.pressPointX = X;
        this.pressPointY = Y;
        calSmallCirclePosition();
        invalidate();
    }

    //手指離開本視圖時,將小圓的圓心則放在視圖中心,然后重新繪制
    private void onFingerUP(){
        smallCirclePointX = bigCirclePointX;
        smallCirclePointY = bigCirclePointY;
        invalidate();
    }

6.繪制控制元件

protected void onDraw(Canvas canvas)protected void dispatchDraw(Canvas canvas) 都是和畫圖有關的方法,都提供了畫布Canvas,區別在于dispatchDraw方法是在onDraw之后呼叫的,故如果自定義控制元件是繼承自ViewGroup時,需要重寫dispatchDraw,避免父控制元件的一些區域被后來繪制的子控制元件遮擋,如果自定義控制元件是繼承自View時,雖然兩方法最終效果相同,不過還是建議重寫onDraw方法,

畫圖嘛,現實生活中,必須要有畫布和畫筆對吧,同樣,在Android中對應Canvas和Paint,
Paint類定義了畫筆的顏色,填充樣式,線條粗細,線條陰影和抗鋸齒等屬性,
Canvas提供了三類方法:①劃定可繪制區域,②繪制各種圖形,③對圖層進行控制操作(如旋轉,縮放,平移,存取圖層),
Canvas的兩個控制操作:
*public int save() * :呼叫之后,會將當前Canvas繪制內容作為一個圖層保存進堆疊中,
public void restore():呼叫之后,將當前繪制的圖層替換為從堆疊頂彈出的圖層,
我們在Java基礎中都學過使用畫布,畫筆來畫圖的知識,這里不再展開多講,只提出幾點與圖層有關的注意項:
①每當呼叫Canvas的drawxxx方法繪制時,都會在Canvas的區域中生成一個大小相同的新的透明圖層,并在這個透明圖層上繪制,繪制結束后,再將這個圖層與Canvas進行疊加,
②對當前圖層進行控制操作后,當前圖層中即將繪制的內容在當前圖層的參考坐標系并不會改變,即始終以當前圖層的左上頂點為坐標原點,向右為X正方向,向下為Y正方向,不過這是相對于當前圖層的自身繪制內容而言的,而相對于Canvas(或者螢屏坐標系)來說,這些繪制的內容的坐標系已經變化了,同時,對當前圖層的控制效果將一直持續影響之后生成的新圖層,
③繪制完成后,當Canvas與螢屏進行合成并顯示時,圖層中超過螢屏范圍的內容將不會顯示,

本例中搖桿控制元件使用onDraw方法繪制外觀的實作如下:

//onDraw函式提供了本視圖占據區域的畫布
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //每次繪制前清空本視圖
        canvas.drawColor(Color.WHITE);
        //繪制搖桿的底盤背景貼圖,按照縮放規則填滿本視圖
        canvas.drawBitmap(rockerPlate, rockerPlateMatrix,rockerPlatePaint);
        //繪制搖桿的球所在的扇形區域
        canvas.drawPath(getBallOfRegion(smallCirclePointX,smallCirclePointY).getBoundaryPath(),rockerSectorPaint);
        //繪制搖桿的桿
        canvas.drawLine(bigCirclePointX,bigCirclePointY,smallCirclePointX,smallCirclePointY,rockerBarPaint);
        //繪制搖桿的球
        canvas.drawCircle(smallCirclePointX,smallCirclePointY,rockerBallRadius,rockerBallPaint);
    }

    //檢測搖桿的球所在的區域
    private Region getBallOfRegion(float smallCirclePointX,float smallCirclePointY){
        for (int i = 0; i < sectorRegions.length; i++) {
           if (sectorRegions[i].contains((int) smallCirclePointX,(int) smallCirclePointY)){
               if (mListener != null){//通知監聽器搖桿球的位置
                   mListener.getSectorOfBall(i);
               }
               return sectorRegions[i];
           }
        }
        //搖桿的球默認在視圖中心,
        return new Region((int) bigCirclePointX,(int) bigCirclePointY,
                (int) bigCirclePointX+1,(int) bigCirclePointY+1);
    }

那么,一個完整的自定義控制元件便橫空出世了,完整的RockerView類代碼如下(絕不是為了水文章字數,,):
在這里插入圖片描述

public class RockerView extends View {
    private Context mContext; // 宣告一個背景關系物件
    //!!搖桿的背景貼圖,扇形區域,桿,球的各種屬性,如果在XML檔案中未指定這些屬性,則使用以下默認值:
    private Bitmap rockerPlate; // 搖桿的底盤的背景貼圖
    private Region[] sectorRegions;//保存搖桿均分的每個區域
    private int rockerSectorNum = 8;//搖桿的底盤默認平均分為8個扇形區域
    private int rockerSectorColor = Color.CYAN;// 搖桿的球所落在區域的顏色
    private int rockerBarColor = Color.GREEN; // 搖桿的桿的顏色
    private int rockerBarWidth = 30; // 搖桿的桿的寬度
    private int rockerBallColor = Color.RED; // 搖桿的球的顏色
    private int rockerBallRadius = 50; // 搖桿的球的半徑(即小圓的半徑:r )
    //!!

    private Matrix rockerPlateMatrix = new Matrix();//此矩陣用于背景貼圖的縮放變換以填滿本視圖
    private Paint rockerPlatePaint = new Paint();//繪制背景貼圖的畫筆
    private Paint rockerSectorPaint = new Paint();//繪制扇形區域的畫筆
    private Paint rockerBarPaint = new Paint();//繪制搖桿的桿的畫筆
    private Paint rockerBallPaint = new Paint();//繪制搖桿的球的畫筆

    public RockerView(Context context) {
        super(context);
    }

    //在含有兩個引數的建構式中獲取XML檔案中設定的該控制元件的屬性值
    public RockerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        if (attrs != null) {
            // 根據RockerView的屬性定義,從布局檔案中獲取屬性陣列
            TypedArray attrArray = mContext.obtainStyledAttributes(attrs, R.styleable.RockerView);
            // 獲取布局檔案中的搖桿的底盤的背景貼圖
            rockerPlate = BitmapFactory.decodeResource(mContext.getResources(),
                    attrArray.getResourceId(R.styleable.RockerView_rocker_plate_background, R.drawable.rocker_plate_background1));
            // 獲取布局檔案中的搖桿平均劃分的扇形區域
            rockerSectorNum = attrArray.getInteger(R.styleable.RockerView_rocker_sector_num, rockerSectorNum);
            // 獲取布局檔案中的搖桿的扇形區域的顏色
            rockerSectorColor = attrArray.getColor(R.styleable.RockerView_rocker_sector_color, rockerSectorColor);
            // 獲取布局檔案中的搖桿的桿的顏色
            rockerBarColor = attrArray.getColor(R.styleable.RockerView_rocker_bar_color, rockerBarColor);
            // 獲取布局檔案中的搖桿的桿的寬度
            rockerBarWidth = attrArray.getInteger(R.styleable.RockerView_rocker_bar_width, rockerBarWidth);
            // 獲取布局檔案中的搖桿的球的顏色
            rockerBallColor = attrArray.getInteger(R.styleable.RockerView_rocker_ball_color, rockerBallColor);
            // 獲取布局檔案中的搖桿的球的半徑
            rockerBallRadius = attrArray.getInteger(R.styleable.RockerView_rocker_ball_radius, rockerBallRadius);

            // 回收屬性陣列
            attrArray.recycle();
        }
        //根據從XML總獲取的屬性值設定相關畫筆,一般繪制圖片的畫筆不需要特別的設定引數
        rockerBarPaint.setAntiAlias(true); // 設定畫筆為無鋸齒
        rockerBarPaint.setDither(true); // 設定畫筆為防抖動
        rockerBarPaint.setColor(rockerBarColor); // 設定畫筆的顏色
        rockerBarPaint.setStrokeWidth(rockerBarWidth); // 設定畫筆的線寬
        rockerBarPaint.setStrokeCap(Paint.Cap.ROUND); //設定線段的端點形狀
        rockerBarPaint.setStyle(Paint.Style.FILL); // 設定畫筆的型別:STROKE表示空心,FILL表示實心
        rockerBallPaint.setAntiAlias(true);
        rockerBallPaint.setDither(true);
        rockerBallPaint.setColor(rockerBallColor);
        rockerBallPaint.setStyle(Paint.Style.FILL);
        rockerSectorPaint.setColor(rockerSectorColor);
        rockerSectorPaint.setStyle(Paint.Style.FILL);
    }

    private final int ROCKER_VIEW_SIZE = 400;//本視圖默認的尺寸大小

    //通過onMeasure函式測量本視圖大小
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         //獲取上級視圖給出的關于本視圖大小的測量模式
         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
         int rockerViewMinSize;//本視圖占據的區域是一個正方形,故長寬應該一致
         if (widthMode == MeasureSpec.UNSPECIFIED ||
                 heightMode == MeasureSpec.UNSPECIFIED){//上級視圖未指定本視圖的尺寸大小,則使用默認的尺寸
             rockerViewMinSize = ROCKER_VIEW_SIZE;
         }else {//若上級視圖給出本視圖確定的寬高尺寸,那么取寬高中最小值作為本視圖的區域
             rockerViewMinSize = Math.min(MeasureSpec.getSize(widthMeasureSpec),
                     MeasureSpec.getSize(heightMeasureSpec));
         }
         //向上級視圖確認提交本視圖的尺寸大小
         setMeasuredDimension(rockerViewMinSize,rockerViewMinSize);
    }

    //使用RectF用來保存搖桿視圖(正方形區域)在螢屏坐標系下的位置(暫時沒用到該物件!)
    private RectF rockerViewRectF = new RectF();
    //正方形的內切圓的半徑和圓心:(X-Xa)^2 + (Y-Ya)^2 = R^2
    private float bigCircleRadius;
    private float bigCirclePointX;
    private float bigCirclePointY;
    //中圓(小圓的圓心軌跡)的半徑
    private float middleCircleRadius;

    //通過onLayout函式確定本視圖在螢屏的位置坐標
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        //獲取搖桿視圖在螢屏坐標系下占據的正方形區域的布局位置
        rockerViewRectF.set(left,top,right,bottom);
        //獲取大圓在本視圖的坐標系下的布局位置
        //大圓半徑為視圖邊長的一半
        bigCircleRadius = (float) Math.min(getMeasuredHeight(),getMeasuredWidth()) / 2;
        //圓心的X坐標為視圖的邊長的一半
        bigCirclePointX = bigCircleRadius;
        //圓心的Y坐標為為視圖的邊長的一半
        bigCirclePointY =  bigCircleRadius;
        /** 注意不要用以下方法求大圓的圓心坐標:
         * bigCirclePointY = top + bigCircleRadius;
         * bigCirclePointX = left + bigCircleRadius;
         *這是由于螢屏的坐標系和視圖的坐標系是分開的,這里只用到視圖的坐標系
         * 視圖的坐標系同樣以視圖左上角為原點,向右為X正方向,向下為Y正方向
         */
        //中圓和大圓是同心圓,且中圓半徑為R-r,中圓的方程:(X-Xa)^2 + (Y-Ya)^2 = (R-r)^2
        middleCircleRadius = bigCircleRadius - rockerBallRadius;

        //縮放背景貼圖來填滿本視圖
        rockerPlateMatrix.reset();
        float scaleX = (float) getMeasuredWidth() / rockerPlate.getWidth();
        float scaleY = (float) getMeasuredHeight() / rockerPlate.getHeight();
        rockerPlateMatrix.setScale(scaleX,scaleY);

        //計算并填充均分的每個扇形區域
        calSectorRegion();

        //開始繪制時,手指還沒有觸摸本視圖,所以搖桿的球默認位于本視圖中心
        onFingerUP();
    }

    //根據扇形區域數量計算的每個扇形區域的坐標資訊
    private void calSectorRegion(){
        sectorRegions = new Region[rockerSectorNum];
        Path[] sectorPaths = new Path[rockerSectorNum];//每個扇形區域的輪廓
        int sweepAngle = 360 / rockerSectorNum;//每個扇形的張開的角度
        for (int i = 0; i < sectorPaths.length; i++) {//勾勒扇形輪廓
            sectorPaths[i] = new Path();
            sectorPaths[i].addArc(0,0,bigCircleRadius * 2,bigCircleRadius * 2,
                    i * sweepAngle,sweepAngle);
            sectorPaths[i].lineTo(bigCirclePointX,bigCirclePointY);
            sectorPaths[i].close();
        }
        //將扇形輪廓通過一定的裁剪填充進對應的區域:clipRegion1是搖桿視圖占據的正方形區域,clipRegion2是圓心不可能在的區域
        Region clipRegion1 = new Region(0,0,(int) bigCircleRadius * 2,(int) bigCircleRadius * 2);
        Region clipRegion2 = new Region();
        //搖桿球的圓心不可能在此圓形區域內
        Path awayBallPath = new Path();
        awayBallPath.addCircle(bigCirclePointX,bigCirclePointX,bigCircleRadius - rockerBallRadius * 2, Path.Direction.CW);
        clipRegion2.setPath(awayBallPath,clipRegion1);
        for (int i = 0; i < sectorRegions.length; i++) {
            sectorRegions[i] = new Region();
            sectorRegions[i].setPath(sectorPaths[i],clipRegion1);
            //取兩區域的差
            sectorRegions[i].op(clipRegion2, Region.Op.DIFFERENCE);
        }
    }

    //手指按壓點b的坐標
    private float pressPointX;
    private float pressPointY;
    //ab連線所在的直線與中圓的交點,即小圓的圓心c的坐標
    private float smallCirclePointX;
    private float smallCirclePointY;

    //計算小圓的圓心坐標
    private void calSmallCirclePosition() {
        //ab所在的直線穿過大圓,必有兩個交點c1,c2
        //交點c1的坐標
        float interPointX1;
        float interPointY1;
        //交點c2的坐標
        float interPointX2;
        float interPointY2;
        //Xc1,2 =Xa±R/√(1+((Ya-Yb)/(Xa-Xb))^2)
        interPointX1 = (float) (bigCirclePointX + (middleCircleRadius / Math.sqrt(1 + Math.pow(((bigCirclePointY - pressPointY) / (bigCirclePointX - pressPointX)), 2))));
        interPointX2 = (float) (bigCirclePointX - (middleCircleRadius / Math.sqrt(1 + Math.pow(((bigCirclePointY - pressPointY) / (bigCirclePointX - pressPointX)), 2))));
        //Yc1,2 =Ya±R/√(1+((Ya-Yb)/(Xa-Xb))^2)*((Ya-Yb))
        interPointY1 = (float) (bigCirclePointY + (middleCircleRadius / Math.sqrt(1 + Math.pow(((bigCirclePointY - pressPointY) / (bigCirclePointX - pressPointX)), 2)) / (bigCirclePointX - pressPointX) * (bigCirclePointY - pressPointY)));
        interPointY2 = (float) (bigCirclePointY - (middleCircleRadius / Math.sqrt(1 + Math.pow(((bigCirclePointY - pressPointY) / (bigCirclePointX - pressPointX)), 2)) / (bigCirclePointX - pressPointX) * (bigCirclePointY - pressPointY)));
        //bc1的長度,bc2的長度
        float bc1 = (float) Math.sqrt(Math.pow(pressPointX - interPointX1, 2) + Math.pow(pressPointY - interPointY1, 2));
        float bc2 = (float) Math.sqrt(Math.pow(pressPointX - interPointX2, 2) + Math.pow(pressPointY - interPointY2, 2));
        //選擇c1和c2中距離按壓點b最近的點
        smallCirclePointX = bc1 < bc2 ? interPointX1 : interPointX2;
        smallCirclePointY = (smallCirclePointX == interPointX1) ? interPointY1 : interPointY2;
    }

    //重寫該函式,獲取手指在該視圖的觸摸資訊
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_MOVE){
            onFingerDown(event.getX(),event.getY());
        }else if (event.getAction() == MotionEvent.ACTION_UP){
            onFingerUP();
        }
        return true;
    }

    //當手指觸摸本視圖時,則重新計算小圓的圓心坐標,然后重新繪制本視圖
    private void onFingerDown(float X, float Y) {
        this.pressPointX = X;
        this.pressPointY = Y;
        calSmallCirclePosition();
        invalidate();
    }

    //手指離開本視圖時,將小圓的圓心則放在視圖中心,然后重新繪制
    private void onFingerUP(){
        smallCirclePointX = bigCirclePointX;
        smallCirclePointY = bigCirclePointY;
        invalidate();
    }

    //onDraw函式提供了本視圖占據區域的畫布
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //每次繪制前清空本視圖
        canvas.drawColor(Color.WHITE);
        //繪制搖桿的底盤背景貼圖,按照縮放規則填滿本視圖
        canvas.drawBitmap(rockerPlate, rockerPlateMatrix,rockerPlatePaint);
        //繪制搖桿的球所在的扇形區域
        canvas.drawPath(getBallOfRegion(smallCirclePointX,smallCirclePointY).getBoundaryPath(),rockerSectorPaint);
        //繪制搖桿的桿
        canvas.drawLine(bigCirclePointX,bigCirclePointY,smallCirclePointX,smallCirclePointY,rockerBarPaint);
        //繪制搖桿的球
        canvas.drawCircle(smallCirclePointX,smallCirclePointY,rockerBallRadius,rockerBallPaint);
    }

    //檢測搖桿的球所在的區域
    private Region getBallOfRegion(float smallCirclePointX,float smallCirclePointY){
        for (int i = 0; i < sectorRegions.length; i++) {
           if (sectorRegions[i].contains((int) smallCirclePointX,(int) smallCirclePointY)){
               if (mListener != null){//通知監聽器搖桿球的位置
                   mListener.getSectorOfBall(i);
               }
               return sectorRegions[i];
           }
        }
        //搖桿的球默認在視圖中心,
        return new Region((int) bigCirclePointX,(int) bigCirclePointY,
                (int) bigCirclePointX+1,(int) bigCirclePointY+1);
    }

    //設定一個監聽小球所在區域的監聽器
    public interface OnRockerBallMoveListener {
        void getSectorOfBall(int whichSector);
    }
    private OnRockerBallMoveListener mListener;
    public void setOnRockerBallMOveListener(OnRockerBallMoveListener listener){
        mListener = listener;
    }

}

在最后加入了一個扇形區域變化監聽器,在Activity頁面代碼使用此控制元件如下:

        RockerView rockerView1 = findViewById(R.id.rockerView1);
        RockerView rockerView2 = findViewById(R.id.rockerView2);
        rockerView1.setOnRockerBallMOveListener(new RockerView.OnRockerBallMoveListener() {
            @Override
            public void getSectorOfBall(int whichSector) {
                Toast.makeText(MainActivity.this, "rockerView1的球當前所在區域是:"+whichSector, Toast.LENGTH_SHORT).show();
            }
        });

編碼程序中,可以實時觀察AS右側視窗的控制元件預覽,如圖是一個完整的搖桿控制元件的預覽:
在這里插入圖片描述
后來者可以基于此代碼改進得到一個更好看,更好用的搖桿控制元件,
我總結的自定義控制元件的內容可能不夠豐富,但至少是準確的,這也是我記筆記的原則:內容可能不全,但一定要準確,這里推薦關于一位自定義控知識的大佬,即《Android自定義控制元件入門與實戰》的作者啟艦,可以買書,也可以看他在CSDN的博文,

三、頁面布局優化

1.減少重復布局

我們在進行頁面UI布局時,有時可能會在布局檔案中重復布局一組控制元件,那么可以把這組控制元件抽離出來作為公共布局檔案,方便其他布局檔案參考,在參考該公共布局時,需要使用include標簽,并將該標簽的layout屬性設定為公共布局檔案的名稱,
公共布局檔案的頂級節點一般使用merge標簽,它表示一個占位的合并標簽,參考該公共布局的父布局將忽略merge,父布局只將該節點下的所有控制元件抽離出來并放置,這樣,APP在渲染界面時會將merge節點下的所有控制元件匯入,但不對merge根布局的尺寸的計算和調整,從而提高界面渲染速度,

舉個簡單的例子說明這兩個標簽的用法,如下,一個公共布局中包含2個按鈕:
在這里插入圖片描述
然后在兩個線性布局中參考該布局,創建一個2X2的按鈕陣列:
在這里插入圖片描述
吶,使用起來就這么簡單,

2.按需加載布局資源

當把視圖的可視屬性設定為View.GONE時,雖然此視圖在螢屏上消失了,但APP在渲染界面時,還是將此視圖加載進記憶體了,這在記憶體緊張的手機里是浪費行為,如果要在渲染開始前就不加載視圖資源,只有當滿足一定條件時才加載,可以使用ViewStub作為父布局,它容納的子布局由layout屬性指定,在APP加載頁面時,ViewStub中的內容并不會預先加載,只有在代碼中顯式呼叫該ViewStub物件的inflate方法,才會將指定的布局加載進記憶體中,
舉個簡單例子說下用法吧,頁面布局如下,可以從右邊的預覽圖看到ViewStub是默認不加載內容的:
在這里插入圖片描述
在Activity代碼中加載ViewStub的內容如下:

        ViewStub vs_common = findViewById(R.id.vs_common);// 從布局檔案中獲取名叫vs_common的占位視圖
        vs_common.inflate(); // 展開占位視圖

3.自定義主題

樣式和主題資源都用于對APP的外觀進行美化,它們的概念類似于Word,即主題包含各種樣式,樣式包含各種格式,
一個樣式代表一組格式的集合,當為某一控制元件設定樣式后,該樣式所包含的所有格式將作用于該控制元件,樣式資源和主題資源都放在放在res/values目錄下的styles.xml檔案中,該檔案根標簽是resources,可以包含多個style子標簽,style標簽既可以作為一個樣式又可以作為一個主題,它有兩個屬性:
①name:樣式或者主題的名稱,
②parent:樣式或者主題的父樣式或父主題,指定之后,獲得父樣式或父主題的所有格式,當然也可以覆寫其中的格式,
每個style標簽包含多個item子標簽,表示一個格式,
大家都能很熟練地為控制元件指定樣式,要為一個Activity頁面使用主題可通過:
①在組態檔AndroidManifest.xml中,在application節點中設定theme屬性,表示對該APP所有的頁面應用該主題,
②同樣在組態檔中,對activity節點設定theme屬性,表示此活動頁面單獨應用該主題,
③在Activity的onCreate方法中,在setContentView方法之前呼叫setTheme方法設定該Activity的主題,

四、自定義通知欄

1.在通知欄顯示通知

大家在日常生活中使用手機時,肯定被部分APP在通知欄推送的各種廣告搞得煩不勝煩(什么大滿減,什么送現金之類的,唉,哪怕要是有一個是真的,我也不會這么窮),那么作為開發者,如何讓自己的APP在通知欄推送訊息呢?
通知 Notification可以在通知欄顯示訊息,它是一種全域效果的通知,開發者一般通過 通知管理器 NotificationManager來推送Notification到通知欄,通知的構造類 Notification.Builder可通過一系列setxxx方法設定Notification的各種屬性,從而最終組合成一個Notification,它的setxxx方法有很多,這里只列舉部分:
Notification.Builder setDefaults(int defaults):選擇哪種通知屬性將使用系統默認值,defaults取值范圍: DEFAULT_SOUND (聲音),DEFAULT_VIBRATE (震動),DEFAULT_LIGHTS(閃光燈),
Notification.Builder setAutoCancel(boolean autoCancel):設定當用戶觸摸該通知時,該通知是否自動消失,
Notification.Builder setContentTitle(CharSequence title):設定通知的標題文字,
Notification.Builder setContentText(CharSequence text):設定通知的內容文字,
Notification.Builder setLargeIcon(Bitmap b)
Notification.Builder setLargeIcon(Icon icon)
:設定通知的大圖示,
Notification.Builder setSmallIcon(int icon, int level)
Notification.Builder setSmallIcon(int icon)
Notification.Builder setSmallIcon(Icon icon)
:設定通知的小圖示,
Notification.Builder setTicker(CharSequence tickerText):設定本條通知在無障礙服務的“ticker”文本,
Notification.Builder setContentIntent(PendingIntent intent):設定單擊通知時將要啟動的組件,
Notification.Builder setContent(RemoteViews views):提供一個自定義遠程視圖RemoteViews來代替Notification.Builder默認的通知樣式模板,
Notification build():組合所有已設定的通知引數并回傳一個 Notification物件,

Android 8之后根據通知的重要程度,劃分了通知渠道 NotificationChannel,從而方便用戶管理泛濫的通知,那么開發者就必須老實的為每條通知分配對應重要程度的渠道,除此之外,通知的聲音,閃光燈,震動都由NotificationChannel管理了,還有在創建Notification.Builder物件時也要指定通知渠道,
那么,發送一條通知的步驟如下:
①呼叫getSystemService方法指定NOTIFICATION_SERVICE服務并獲取通知管理器NotificationManager的實體物件,
②創建通知渠道 NotificationChannel,并在管理器中宣告該渠道,
③通過構造器創建通知構造類Notification.Builder物件,
④為Notification.Builder物件設定各種引數,并呼叫build方法生成Notification實體物件,
⑤通過NotificationManager結合通知渠道發送Notification,
那么基于以上步驟,得到一個通用的發送通知的模板如下:

// 發送一條通知到手機的通知欄(包括通知標題和通知內容)
    private void sendOneNotification(String title, String message) {
        // 創建一個跳轉到活動頁面的意圖
        Intent clickIntent = new Intent(this, MainActivity.class);
        // 創建一個用于頁面跳轉的延遲意圖
        PendingIntent contentIntent = PendingIntent.getActivity(this,
                R.string.app_name, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        // 創建一個通知的構造器
        Notification.Builder builder = new Notification.Builder(this);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // Android 8.0開始必須給每個通知分配對應的渠道
            builder = new Notification.Builder(this, getString(R.string.app_name));
        }
        builder.setContentIntent(contentIntent) // 設定內容的點擊意圖
                .setAutoCancel(true) // 設定是否允許自動清除
                .setSmallIcon(R.mipmap.ic_launcher) // 設定狀態里的小圖示
                //.setSubText("副本文字") // 設定通知里面的附加說明文本
                .setTicker("提示文字") // 設定通知里面的提示文本
                .setWhen(System.currentTimeMillis()) // 設定推送時間,格式為“小時:分鐘”
                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) // 設定通知里面的大圖示
                .setContentTitle(title) // 設定通知里面的標題文本
                .setContentText(message); // 設定通知里面的內容文本
        Notification notify = builder.build();// 根據通知構造器構建一個通知物件
        // 從系統服務中獲取通知管理器
        NotificationManager notifyMgr = (NotificationManager)
                getSystemService(Context.NOTIFICATION_SERVICE);
        // 創建指定的通知渠道
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // 創建一個默認重要性的通知渠道
            NotificationChannel channel = new NotificationChannel(getString(R.string.app_name),
                    "Channel", NotificationManager.IMPORTANCE_DEFAULT);
            channel.setSound(null, null); // 設定推送通知之時的鈴聲,null表示靜音推送
            channel.enableLights(true); // 設定在桌面圖示右上角展示小紅點
            channel.setLightColor(Color.RED); // 設定小紅點的顏色
            channel.setShowBadge(true); // 在長按桌面圖示時顯示該渠道的通知
            notifyMgr.createNotificationChannel(channel);//在通知管理器中宣告該渠道
        }
        // 使用通知管理器推送通知,然后在手機的通知欄就會看到
        notifyMgr.notify(R.string.app_name, notify);
    }

那么通過該方法推送一條通知,效果如下,當點擊該通知后,通知消失,然后會跳轉到Intent 指定的組件中:
在這里插入圖片描述

2.自定義通知欄的視圖

通知可以通過setContent方法設定自定義的遠程視圖 RemoteViews來代替Notification.Builder默認的通知樣式模板,Android界面中要用到RemoteViews的場景主要在通知欄和桌面,而且RemoteViews只支持內嵌幾種布局:AdapterViewFlipper,FrameLayout,GridLayout,GridView,LinearLayout,ListView,RelativeLayout,StackView,ViewFlipper,只支持內嵌幾種控制元件:TextView,ImageView,Button,ProgressBar,Chronometer,AnalogClock,而且不支持內嵌第三方控制元件,這些內嵌控制元件的內容只能通過RemoteViews物件的setxxx方法修改,RemoteViews的常用方法如下:
RemoteViews(String packageName, int layoutId):構造方法,packageName是包名,layoutId是布局檔案ID,
void setViewVisibility(int viewId, int visibility):設定指定ID的控制元件是否可見,
void setViewPadding(int viewId, int left, int top, int right, int bottom):設定指定ID的控制元件的內邊距,
void setTextViewText(int viewId, CharSequence text)
void setTextViewTextSize(int viewId, int units, float size)
:設定指定ID的文本視圖或者按鈕的文字和大小,
void setTextColor(int viewId, int color):設定指定ID控制元件的文字顏色,
void setTextViewCompoundDrawables(int viewId, int left, int top, int right, int bottom)
void setTextViewCompoundDrawablesRelative(int viewId, int start, int top, int end, int bottom)
:設定指定ID的文本視圖的四周的圖示,
void setImageViewResource(int viewId, int srcId):設定指定ID的圖形視圖的影像來源,
void setChronometer(int viewId, long base, String format, boolean started):設定指定ID的計時器資訊,
void setProgressBar(int viewId, int max, int progress, boolean indeterminate):設定指定ID的進度條的資訊,
void setOnClickPendingIntent(int viewId, PendingIntent pendingIntent):設定指定ID的控制元件的點擊回應意圖,
接下來舉個例子熟悉一下自定義通知欄吧,比如在通知欄顯示一個音樂播放狀態的通知,
首先,通知內容的布局檔案notify_music.xml如下:
在這里插入圖片描述
其次,在代碼中發送該通知內容到通知欄的方法如下:

    private Notification getNotification(Context ctx, String event, String song, boolean isPlaying, int progress, long time) {
        // 創建一個廣播事件的意圖
        Intent intent1 = new Intent(event);
        // 創建一個用于廣播的延遲意圖
        PendingIntent broadIntent = PendingIntent.getBroadcast(
                ctx, R.string.app_name, intent1, PendingIntent.FLAG_UPDATE_CURRENT);
        // 根據布局檔案notify_music.xml生成遠程視圖物件
        RemoteViews notify_music = new RemoteViews(ctx.getPackageName(), R.layout.notify_music);
        if (isPlaying) { // 正在播放
            notify_music.setTextViewText(R.id.btn_play, "暫停"); // 設定按鈕文字
            notify_music.setTextViewText(R.id.tv_play, song + "正在播放"); // 設定文本文字
            notify_music.setChronometer(R.id.chr_play, time, "%s", true); // 設定計數器
        } else { // 不在播放
            notify_music.setTextViewText(R.id.btn_play, "繼續"); // 設定按鈕文字
            notify_music.setTextViewText(R.id.tv_play, song + "暫停播放"); // 設定文本文字
            notify_music.setChronometer(R.id.chr_play, time, "%s", false); // 設定計數器
        }
        // 設定遠程視圖內部的進度條屬性
        notify_music.setProgressBar(R.id.pb_play, 100, progress, false);
        // 設定單個控制元件的點擊廣播意圖,一旦點擊該控制元件,就發出對應事件的廣播,具體回呼處理則可定義一個廣播接收器,本例不再展開舉例
        notify_music.setOnClickPendingIntent(R.id.btn_play, broadIntent);
        // 創建一個跳轉到活動頁面的意圖
        Intent intent2 = new Intent(ctx, MainActivity.class);
        // 創建一個用于頁面跳轉的延遲意圖
        PendingIntent clickIntent = PendingIntent.getActivity(ctx,
                R.string.app_name, intent2, PendingIntent.FLAG_UPDATE_CURRENT);
        // 創建一個通知訊息的構造器
        Notification.Builder builder = new Notification.Builder(ctx);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // Android 8.0開始必須給每個通知分配對應的渠道
            builder = new Notification.Builder(ctx, getString(R.string.app_name));
        }
        builder.setContentIntent(clickIntent) // 設定內容的點擊意圖
                .setContent(notify_music) // 設定內容視圖
                .setTicker(song) // 設定狀態欄里面的提示文本
                .setSmallIcon(R.drawable.qq); // 設定狀態欄里的小圖示
        // 根據訊息構造器構建一個通知物件
        return builder.build();
    }

    private String PAUSE_EVENT = ""; // “暫停/繼續”事件的標識串

    private void sendSongNotification(String songName){
        // 獲取自定義訊息的通知物件
        Notification notify = getNotification(this, PAUSE_EVENT,songName, true, 50, SystemClock.elapsedRealtime());
        // 從系統服務中獲取通知管理器
        NotificationManager notifyMgr = (NotificationManager)
                getSystemService(Context.NOTIFICATION_SERVICE);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // 創建一個默認重要性的通知渠道
            NotificationChannel channel = new NotificationChannel(getString(R.string.app_name),
                    "Channel", NotificationManager.IMPORTANCE_DEFAULT);
            channel.setSound(null, null); // 設定推送通知之時的鈴聲,null表示靜音推送
            channel.enableLights(true); // 設定在桌面圖示右上角展示小紅點
            channel.setLightColor(Color.RED); // 設定小紅點的顏色
            channel.setShowBadge(true); // 在長按桌面圖示時顯示該渠道的通知
            notifyMgr.createNotificationChannel(channel);//在通知管理器中宣告該渠道
        }
        // 使用通知管理器推送通知,然后在手機的通知欄就會看到該訊息
        notifyMgr.notify(R.string.app_name, notify);
    }

最后在通知欄顯示的音樂播放狀態的通知效果如圖:
在這里插入圖片描述
本例沒有對播放控制按鈕添加相關的回呼方法,開發者可自定義一個廣播接收器來接收廣播并進行相關回呼處理,

五、碎片

使用碎片 Fragment是為了更好的適應大螢屏的平板,由于平板的頁面可以容納更多的UI控制元件,不過隨之而來的問題是處理控制元件之間更加復雜的互動,Fragment可以對UI組件進行分組,模塊化管理,讓開發者可以宏觀的洗掉,替換,添加螢屏的某一區域內容,從而更方便的動態更新Activity的界面,
Fragment是Activity的一個子模塊,有自己的生命周期,但同時受到宿主Activity的生命周期的影響,如宿主Activity暫停時,寄生的Fragment也會暫停,宿主Activity銷毀時,寄生的Fragment也會銷毀,一個宿主Activity可以容納多個寄生的Fragment,當然一個Fragment可以被多個宿主Activity使用,

1.Fragment的生命周期

Fragment作為Activity的寄生體,Activity擁有的所有生命周期回呼方法,寄生的Fragment當然也擁有,除此之外,Fragment還多出了五個額外的生命周期方法:
public void onAttach(@NonNull Context context):寄生的Fragment與宿主Activity結合時回呼,該方法只會被呼叫一次,可以在這里獲取宿主Activity的實體物件,
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState):每次創建Fragment的視圖內容時回呼該此方法,
public void onActivityCreated(@Nullable Bundle savedInstanceState):當宿主Activity創建之后回呼,
public void onDestroyView():銷毀該Fragment的視圖內容時回呼該此方法,
public void onDetach():當寄生的Fragment被宿主Activity洗掉,替換時回呼此方法,
Fragment具體的生命周期方法的呼叫順序可通過日志列印來觀察,這里不再詳述,

2.Fragment的管理

一個宿主Activity中包含多個寄生的Fragment,可以通過碎片管理器 FragmentManager使用碎片堆疊來管理,FragmentManager的常用方法有:
abstract Fragment findFragmentById(int id)
abstract Fragment findFragmentByTag(String tag)
:通過ID或者標簽從宿主Activity中查找獲取Fragment實體物件,
abstract void popBackStack():從堆疊中彈出堆疊頂的Fragment實體物件,用戶按手機的回傳鍵也和該方法效果相同,
abstract void addOnBackStackChangedListener(FragmentManager.OnBackStackChangedListener listener):為碎片堆疊添加一個狀態變化監聽器,
abstract FragmentTransaction beginTransaction():在與FragmentManager關聯的碎片上開始一系列編輯操作,即獲取一個碎片事務,
要添加,洗掉,替換Fragment,需要使用碎片事務 FragmentTransaction,這里的事務的概念和資料庫中的事務概念相同,它的常用方法有:
FragmentTransaction add (int containerViewId, Fragment fragment, String tag):向宿主Activity添加一個Fragment ,
abstract FragmentTransaction addToBackStack(String name):將一個Fragment 添加進堆疊中,
abstract FragmentTransaction remove(Fragment fragment):從宿主Activity洗掉一個Fragment ,
abstract FragmentTransaction replace(int containerViewId, Fragment fragment, String tag):替換宿主Activity中的一個Fragment ,
abstract int commit():提交碎片事務,

3.Fragment的使用

Fragment的創建程序和Activity類似,都需要有類和布局,碎片類可以通過繼承Fragment基類,重寫一部分方法來定義自己的碎片,之后創建一個與碎片類對應的布局檔案,顯然,手動創建太麻煩了,我們可通過AS自動創建常見頁面布局的碎片,只在需要存放碎片的目錄下滑鼠右鍵->New->Fragment,然后出現:
在這里插入圖片描述
可以看到支持自動生成的碎片種類其實不多,當我們點擊其中一個種類的碎片時,AS會彈出一個配置視窗供我們設定碎片的頁面布局,名稱等初始引數,分別點擊空白,串列,登錄種類的碎片的配置視窗如下:
在這里插入圖片描述
這里我們定義一個空白碎片BlankFragment,空白碎片的頁面完全自定義,擴展性很強,當我們點擊配置視窗的Finish后,AS會自動生成BlankFragment的部分代碼,并且給出了注釋,我們只需按照這些提示填充自定義碎片的代碼就行了,看一下AS幫我們自動生成的代碼中:

    /**
     * Use this factory method to create a new instance of
     * this fragment using the provided parameters.
     *
     * @param param1 Parameter 1.
     * @param param2 Parameter 2.
     * @return A new instance of fragment BlankFragment.
     */
    // TODO: Rename and change types and number of parameters
    public static BlankFragment newInstance(String param1, String param2) {
        BlankFragment fragment = new BlankFragment();
        Bundle args = new Bundle();
        args.putString(ARG_PARAM1, param1);
        args.putString(ARG_PARAM2, param2);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mParam1 = getArguments().getString(ARG_PARAM1);
            mParam2 = getArguments().getString(ARG_PARAM2);
        }
    }

按照注釋,我們需要使用該類的newInstance方法傳遞相關引數并生成碎片實體物件,該方法是先通過建構式生成碎片物件,然后呼叫它的setArguments方法設定引數的,在生成物件的程序中,即碎片生命周期onCreate方法中通過getArguments獲取設定的引數,然后根據這些引數設定碎片物件,

與使用普通的控制元件視圖相比,Fragment的作用還是在螢屏顯示內容,該內容就是在創建Fragment時指定的與之關聯的布局檔案,所以它就是一個特別一點點的視圖而已,Fragment既可以直接在布局檔案中使用,也可以在Java代碼中手動創建并添加到螢屏上,

在布局檔案中通過添加fragment標簽,然后指定該碎片的id,name(碎片的全路徑類名)和其他的布局屬性就可以了,和普通控制元件沒啥大的區別,

這里舉個例子來說明在Java代碼中動態創建Fragment,讓Fragment搭配前文的翻頁視圖ViewPager共同使用,如此,翻頁視圖的每一頁就是一個Fragment,

首先需要修改一下自動生成的空白碎片BlankFragment的頁面布局,插入一個圖形視圖和文本視圖:
在這里插入圖片描述
其次需要根據碎片的布局顯示的內容,修改一下AS自動生成的BlankFragment類:

public class BlankFragment extends Fragment {
    protected View mView; // 宣告一個視圖物件
    protected Context mContext; // 宣告一個背景關系物件
    private int mImageId; // 圖片的資源編號
    private String mDesc; // 文字描述

    // 獲取該碎片的一個實體
    public static BlankFragment newInstance(int image_id, String desc) {
        BlankFragment fragment = new BlankFragment(); // 創建該碎片的一個實體
        Bundle bundle = new Bundle(); // 創建一個新包裹
        bundle.putInt("image_id", image_id); // 往包裹存入圖片的資源編號
        bundle.putString("desc", desc); // 往包裹存入軟體的文字描述
        fragment.setArguments(bundle); // 把包裹塞給碎片
        return fragment; // 回傳碎片實體
    }

    // 創建碎片視圖
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mContext = getActivity(); // 獲取活動頁面的背景關系
        if (getArguments() != null) { // 如果碎片攜帶有包裹,則打開包裹獲取引數資訊
            mImageId = getArguments().getInt("image_id", 0);
            mDesc = getArguments().getString("desc");
        }
        // 根據布局檔案fragment_blank.xml生成視圖物件
        mView = inflater.inflate(R.layout.fragment_blank, container, false);
        ImageView iv_pic = mView.findViewById(R.id.iv_pic);
        TextView tv_desc = mView.findViewById(R.id.tv_desc);
        iv_pic.setImageResource(mImageId);
        tv_desc.setText(mDesc);
        return mView; // 回傳該碎片的視圖物件
    }
}

與碎片搭配的翻頁視圖需要使用另一個專門的配接器FragmentStatePagerAdapter ,通過繼承該配接器,實作的代碼如下:

public class SoftwareFragmentPagerAdapter extends FragmentStatePagerAdapter {
    private ArrayList<SoftwareBean> mSoftwareList; // 宣告一個工具軟體佇列
    // 碎片頁配接器的建構式,傳入碎片管理器與軟體佇列
    public SoftwareFragmentPagerAdapter(FragmentManager fm, ArrayList<SoftwareBean> software_list) {
        super(fm);
        mSoftwareList = software_list;
    }

    // 獲取碎片Fragment的個數
    public int getCount() {
        return mSoftwareList.size();
    }

    // 獲取指定位置的碎片Fragment
    public Fragment getItem(int position) {
        return BlankFragment.newInstance(mSoftwareList.get(position).image, mSoftwareList.get(position).desc);
    }

    // 獲得指定碎片頁的標題文本
    public CharSequence getPageTitle(int position) {
        return mSoftwareList.get(position).name;
    }
}

最后使用這對組合時,只需在Activity的頁面布局中添加一個翻頁視圖和一個翻頁標題欄:
在這里插入圖片描述
在Activity中初始化這對組合的代碼如下:

 // 初始化翻頁視圖
    private void initViewPager() {
        ArrayList<SoftwareBean> goodsList = SoftwareBean.getDefaultList();
        // 構建一個碎片翻頁配接器
        SoftwareFragmentPagerAdapter adapter = new SoftwareFragmentPagerAdapter(
                getSupportFragmentManager(), goodsList);
        // 從布局視圖中獲取名叫vp_content的翻頁視圖
        ViewPager vp_content = findViewById(R.id.vp_content);
        // 給vp_content設定碎片配接器
        vp_content.setAdapter(adapter);
        // 設定vp_content默認顯示第一個頁面
        vp_content.setCurrentItem(0);
    }

最終的顯示效果其實和前面效果一樣,不過是將翻頁視圖的每一頁換成了碎片而已:
在這里插入圖片描述

4.Fragment與Activity通信

宿主Activity和寄生的Fragment通常需要雙向傳送資料,比如Fragment中的按鈕被點擊時要及時將該點擊資訊發送給Activity,Activity作出回應后,要改變Fragment中某一控制元件的內容時也要向Fragment發送資料,通常,有兩種方法可以實作它們之間的雙向通信:
(1)方法一
寄生的Fragment可通過getActivity方法獲取宿主Activity的實體,宿主Activity可通過與自身關聯的碎片管理器的findFragmentById或者findFragmentByTag方法獲取寄生的Fragment的實體,于是,如果宿主Activity需要向寄生的Fragment發送資料,則可呼叫Fragment物件的的setArguments方法,如果寄生的Fragment需要向宿主Activity發送資料,則可以定義一個內部回呼介面,再讓宿主Activity實作該介面就可以實作通信了,
(2)方法二
估計小部分開發者看到方法一覺得稍微有點難度,其實由于Fragment也有生命周期,故使用廣播來實作雙向通信就很簡單了,此外,廣播還可以實作Fragment與Fragment之間,Fragment與其他組件的通信,這里就不再舉例了,
所以說,Android的廣播真的是一種簡單,高效的通信方式,面對這種相互通信的場景時,別老想著定義回呼介面,要優先考慮廣播,
在這里插入圖片描述


總結

本文粗略的總結了螢屏顯示,自定義控制元件,頁面布局優化,自定義通知欄,碎片五個部分,常用控制元件從常識篇到高級篇共四篇文章足以應對工具類APP的GUI開發了,還是那句話,控制元件的學習要先建立感性的認識,要知道該控制元件有著怎樣的外觀和作用,其次才是學習關于它的API,這樣的話,假如有一天去別的平臺如Windows或Linux或嵌入式等,它們的GUI部分也是大同小異的,學習起來也是觸類旁通的,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/353442.html

標籤:其他

上一篇:[RK3568 Android11] 教程之parameter新建磁區

下一篇:Android dump渲染和合成圖層GraphicBuffer指南

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more