-
1.自定義view的分類
- 自定義單一view(不含子view)
- 繼承view
- 繼承特定view如textview
- 自定義viewGroup(含子view)
- 繼承viewGroup
- 繼承特定的viewGroup如LinearLayout
- 自定義單一view(不含子view)
-
2.使用注意點
- 支持特殊屬性
- wrap_content
- padding
- margin
- 多線程直接使用post方式
- 避免使用handler等其他方式
- 避免內(nèi)存泄露
- 線程/動畫要及時停止
- 處理好滑動沖突
- view帶有滑動嵌套情況
- 支持特殊屬性
-
3.具體實例
- 實現(xiàn)基本自定義view
- 支持wrap_content屬性
- 支持padding屬性
- 提供自定義屬性
1 自定義View的分類
自定義View一共分為兩大類,具體如下圖:
image
2 具體介紹 & 使用場景
image
3 使用注意點
image
3.1 支持特殊屬性--wrap_content
- 支持wrap_content
如果不在onMeasure()中對wrap_content作特殊處理,那么wrap_content屬性將失效
自定義View2---View Measure過程 - 在onMeasure()中的
getDefaultSize()
的默認實現(xiàn)中,當View的測量模式是AT_MOST或EXACTLY時,View的大小都會被設置成子View MeasureSpec的specSize。 - 因為AT_MOST對應wrap_content;EXACTLY對應match_parent,所以,默認情況下,wrap_content和match_parent是具有相同的效果的。
- 在計算子View MeasureSpec的
getChildMeasureSpec()
中,子View MeasureSpec在屬性被設置為wrap_content或match_parent情況下,子View MeasureSpec的specSize被設置成parenSize = 父容器當前剩余空間大小 - 所以:wrap_content起到了和match_parent相同的作用:等于父容器當前剩余空間大小
3.1.1 默認情況getDefaultSize()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//參數(shù)說明:View的寬 / 高測量規(guī)格
//setMeasuredDimension() 用于獲得View寬/高的測量值
//這兩個參數(shù)是通過getDefaultSize()獲得的
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
//參數(shù)說明:
// 第一個參數(shù)size:提供的默認大小
// 第二個參數(shù):寬/高的測量規(guī)格(含模式 & 測量大小)
//設置默認大小
int result = size;
//獲取寬/高測量規(guī)格的模式 & 測量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
// 模式為UNSPECIFIED時,使用提供的默認大小
// 即第一個參數(shù):size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// 模式為AT_MOST,EXACTLY時,使用View測量后的寬/高值
// 即measureSpec中的specSize
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
//返回View的寬/高值
return result;
}
3.1.2 getChildMeasureSpec()
//作用:
/ 根據(jù)父視圖的MeasureSpec & 布局參數(shù)LayoutParams,計算單個子View的MeasureSpec
//即子view的確切大小由兩方面共同決定:父view的MeasureSpec 和 子view的LayoutParams屬性
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//參數(shù)說明
* @param spec 父view的詳細測量值(MeasureSpec)
* @param padding view當前尺寸的的內(nèi)邊距和外邊距(padding,margin)
* @param childDimension 子視圖的布局參數(shù)(寬/高)
//父view的測量模式
int specMode = MeasureSpec.getMode(spec);
//父view的大小
int specSize = MeasureSpec.getSize(spec);
//通過父view計算出的子view = 父大小-邊距(父要求的大小,但子view不一定用這個值)
int size = Math.max(0, specSize - padding);
//子view想要的實際大小和模式(需要計算)
int resultSize = 0;
int resultMode = 0;
//通過父view的MeasureSpec和子view的LayoutParams確定子view的大小
// 當父view的模式為EXACITY時,父view強加給子view確切的值
//一般是父view設置為match_parent或者固定值的ViewGroup
switch (specMode) {
case MeasureSpec.EXACTLY:
// 當子view的LayoutParams>0,即有確切的值
if (childDimension >= 0) {
//子view大小為子自身所賦的值,模式大小為EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
// 當子view的LayoutParams為MATCH_PARENT時(-1)
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view大小為父view大小,模式為EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
// 當子view的LayoutParams為WRAP_CONTENT時(-2)
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view決定自己的大小,但最大不能超過父view,模式為AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當父view的模式為AT_MOST時,父view強加給子view一個最大的值。(一般是父view設置為wrap_content)
case MeasureSpec.AT_MOST:
// 道理同上
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當父view的模式為UNSPECIFIED時,父容器不對view有任何限制,要多大給多大
// 多見于ListView、GridView
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子view大小為子自身所賦的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因為父view為UNSPECIFIED,所以MATCH_PARENT的話子類大小為0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 因為父view為UNSPECIFIED,所以WRAP_CONTENT的話子類大小為0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
image
3.1.3解決方案:
- 當自定義View的布局參數(shù)設置成wrap_content時時,指定一個默認大小(寬 / 高)。
- 具體是在復寫onMeasure()里進行設置
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 獲取寬-測量規(guī)則的模式和大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 獲取高-測量規(guī)則的模式和大小
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 設置wrap_content的默認寬 / 高值
// 默認寬/高的設定并無固定依據(jù),根據(jù)需要靈活設置
// 類似TextView,ImageView等針對wrap_content均在onMeasure()對設置默認寬 / 高值有特殊處理,具體讀者可以自行查看
int mWidth = 400;
int mHeight = 400;
// 當布局參數(shù)設置為wrap_content時,設置默認值
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, mHeight);
// 寬 / 高任意一個布局參數(shù)為= wrap_content時,都設置默認值
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, heightSize);
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(widthSize, mHeight);
}
}
這樣,當你的自定義View的寬 / 高設置成wrap_content屬性時就會生效了。
3.2 支持特殊屬性-支持padding & margin
如果不支持,那么padding和margin(ViewGroup情況)的屬性將失效
- 對于繼承View的控件,padding是在draw()中處理
- 對于繼承ViewGroup的控件,padding和margin會直接影響measure和layout過程
// 復寫onDraw()
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
// 獲取控件的高度和寬度
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
// 設置圓的半徑 = 寬,高最小值的2分之1
int r = Math.min(width, height) / 2;
// 畫出圓(藍色)
// 圓心 = 控件的中央,半徑 = 寬,高最小值的2分之1
canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, r, mPaint1);
}
3.3 多線程應直接使用post方式
View的內(nèi)部本身提供了post系列的方法,完全可以替代Handler的作用,使用起來更加方便、直接。
3.4 避免內(nèi)存泄露
主要針對View中含有線程或動畫的情況:當View退出或不可見時,記得及時停止該View包含的線程和動畫,否則會造成內(nèi)存泄露問題。
啟動或停止線程/ 動畫的方式:
- 啟動線程/ 動畫:使用
view.onAttachedToWindow()
,因為該方法調(diào)用的時機是當包含View的Activity啟動的時刻- 停止線程/ 動畫:使用
view.onDetachedFromWindow()
,因為該方法調(diào)用的時機是當包含View的Activity退出或當前View被remove的時刻
3.5 處理好滑動沖突
當View帶有滑動嵌套情況時,必須要處理好滑動沖突,否則會嚴重影響View的顯示效果。
3.6 提供自定義屬性
使用步驟有如下:
- 在values目錄下創(chuàng)建自定義屬性的xml文件
- 在自定義View的構(gòu)造方法中解析自定義屬性的值
- 在布局文件中使用自定義屬性
3.6.1 步驟1:在values目錄下創(chuàng)建自定義屬性的xml文件
attrs_circle_view.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--自定義屬性集合:CircleView-->
<!--在該集合下,設置不同的自定義屬性-->
<declare-styleable name="CircleView">
<!--在attr標簽下設置需要的自定義屬性-->
<!--此處定義了一個設置圖形的顏色:circle_color屬性,格式是color,代表顏色-->
<!--格式有很多種,如資源id(reference)等等-->
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
對于自定義屬性類型 & 格式如下:
<-- 1. reference:使用某一資源ID -->
<declare-styleable name="名稱">
<attr name="background" format="reference" />
</declare-styleable>
// 使用格式
// 1. Java代碼
private int ResID;
private Drawable ResDraw;
ResID = typedArray.getResourceId(R.styleable.SuperEditText_background, R.drawable.background); // 獲得資源ID
ResDraw = getResources().getDrawable(ResID); // 獲得Drawble對象
// 2. xml代碼
<ImageView
android:layout_width="42dip"
android:layout_height="42dip"
app:background="@drawable/圖片ID" />
<-- 2. color:顏色值 -->
<declare-styleable name="名稱">
<attr name="textColor" format="color" />
</declare-styleable>
// 格式使用
<TextView
android:layout_width="42dip"
android:layout_height="42dip"
android:textColor="#00FF00" />
<-- 3. boolean:布爾值 -->
<declare-styleable name="名稱">
<attr name="focusable" format="boolean" />
</declare-styleable>
// 格式使用
<Button
android:layout_width="42dip"
android:layout_height="42dip"
android:focusable="true" />
<-- 4. dimension:尺寸值 -->
<declare-styleable name="名稱">
<attr name="layout_width" format="dimension" />
</declare-styleable>
// 格式使用:
<Button
android:layout_width="42dip"
android:layout_height="42dip" />
<-- 5. float:浮點值 -->
<declare-styleable name="AlphaAnimation">
<attr name="fromAlpha" format="float" />
<attr name="toAlpha" format="float" />
</declare-styleable>
// 格式使用
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.7" />
<-- 6. integer:整型值 -->
<declare-styleable name="AnimatedRotateDrawable">
<attr name="frameDuration" format="integer" />
<attr name="framesCount" format="integer" />
</declare-styleable>
// 格式使用
<animated-rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:frameDuration="100"
android:framesCount="12"
/>
<-- 7. string:字符串 -->
<declare-styleable name="MapView">
<attr name="apiKey" format="string" />
</declare-styleable>
// 格式使用
<com.google.android.maps.MapView
android:apiKey="0jOkQ80oD1JL9C6HAja99uGXCRiS2CGjKO_bc_g" />
<-- 8. fraction:百分數(shù) -->
<declare-styleable name="RotateDrawable">
<attr name="pivotX" format="fraction" />
<attr name="pivotY" format="fraction" />
</declare-styleable>
// 格式使用
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:pivotX="200%"
android:pivotY="300%"
/>
<-- 9. enum:枚舉值 -->
<declare-styleable name="名稱">
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
</declare-styleable>
// 格式使用
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
<-- 10. flag:位或運算 -->
<declare-styleable name="名稱">
<attr name="windowSoftInputMode">
<flag name="stateUnspecified" value="0" />
<flag name="stateUnchanged" value="1" />
<flag name="stateHidden" value="2" />
<flag name="stateAlwaysHidden" value="3" />
<flag name="stateVisible" value="4" />
<flag name="stateAlwaysVisible" value="5" />
<flag name="adjustUnspecified" value="0x00" />
<flag name="adjustResize" value="0x10" />
<flag name="adjustPan" value="0x20" />
<flag name="adjustNothing" value="0x30" />
</attr>
</declare-styleable>、
// 使用
<activity
android:name=".StyleAndThemeActivity"
android:label="@string/app_name"
android:windowSoftInputMode="stateUnspecified | stateUnchanged | stateHidden" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<-- 特別注意:屬性定義時可以指定多種類型值 -->
<declare-styleable name="名稱">
<attr name="background" format="reference|color" />
</declare-styleable>
// 使用
<ImageView
android:layout_width="42dip"
android:layout_height="42dip"
android:background="@drawable/圖片ID|#00FF00" />
3.6.2 步驟2:在自定義View的構(gòu)造方法中解析自定義屬性的值
// 自定義View的三個構(gòu)造函數(shù)
public CircleView(Context context) {
super(context);
// 在構(gòu)造函數(shù)里初始化畫筆的操作
init();
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs,0);
init();
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 加載自定義屬性集合CircleView
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView);
// 解析集合中的屬性circle_color屬性
// 該屬性的id為:R.styleable.CircleView_circle_color
// 第二個參數(shù)是默認設置顏色
mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);
// 解析后釋放資源
a.recycle();
init();
}
// 畫筆初始化
private void init() {
// 創(chuàng)建畫筆
mPaint1 = new Paint();
// 設置畫筆顏色為藍色
mPaint1.setColor(mColor);
// 設置畫筆寬度為10px
mPaint1.setStrokeWidth(5f);
//設置畫筆模式為填充
mPaint1.setStyle(Paint.Style.FILL);
}
3.6.3 步驟3:在布局文件中使用自定義屬性
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<!--必須添加schemas聲明才能使用自定義屬性-->
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.kailing.diy_view.MainActivity"
>
<!-- 注意添加自定義View組件的標簽名:包名 + 自定義View類名-->
<!-- 控件背景設置為黑色-->
<com.kailing.diy_view.CircleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#000000"
android:padding="30dp"
<!--設置自定義顏色-->
app:circle_color="#FF4081"
/>
</RelativeLayout>