自定義View從入門到放棄(一)
轉至 鴻洋大神 Android 自定義View (一)
效果圖,類似TextView控件,增加點擊時產生隨機數。
隨便嘮叨幾句,學Android也有兩年了,現在纔來真正的學習自定義View,真夠搞笑的。
自定義View步驟:
1、attrs.xml 中自定義View的屬性
2、在View的構造方法中獲取我們的屬性
3、重新onMesure
4、重寫onDraw
第三步不是必須的。
準備工作
1、分析需要定義的屬性
從效果圖來看,我們要添加的屬性大致需要這幾個 textContent
、textSize
、textColor
2、實現邏輯
需求比較簡單,就是點擊切換隨機數,實現view的點擊事件即可
開始
1、在res/values目錄下新建 attrs.xml文件添加自定義屬性
<declare-styleable name="CustomViewStyle">
<attr name="textSize" format="dimension" />
<attr name="textColor" format="color" />
<attr name="textContent" format="string" />
</declare-styleable>
從name可以看出自定義字體顏色、字體大小、字體內容三個屬性,其中format分別表示該屬性可取值的類型
這裏留個坑,後面針對format去整理一下
2、新建View,在構造方法中獲取屬性值
public class CustomTextView extends View {
private static final String TAG = "CustomView";
/**
* 文本內容
*/
private String mTextContent;
/**
* 文本顏色
*/
private int mTextColor;
/**
* 文本大小
*/
private float mTextSize;
public Context mContext;
private Rect mBound;
/**
* 畫筆
*/
private Paint mPaint;
/**
* 直接new一個Custom View 實例的時候,會調用第一個構造函數
*/
public CustomTextView(Context context) {
this(context, null);
}
/**
* 在xml佈局文件中調用Custom View的時候,會調用第二個構造函數
*/
public CustomTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
LogUtil.d(TAG + "--CustomView 2");
}
public CustomTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LogUtil.d(TAG + "--CustomView 3");
// 方式一
// TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomViewStyle, defStyleAttr, 0);
// 方式二
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomViewStyle);
mContext = context;
initView(context, typedArray);
}
public void initView(Context context, TypedArray typedArray) {
// 獲取設置屬性
mTextContent = typedArray.getString(R.styleable.CustomViewStyle_textContent);
mTextColor = typedArray.getColor(R.styleable.CustomViewStyle_textColor, ContextCompat.getColor(context, R.color.colorAccent));
mTextSize = typedArray.getDimension(R.styleable.CustomViewStyle_textSize, 15);
// typedArray 回收
typedArray.recycle();
mPaint = new Paint();
mPaint.setTextSize(mTextSize);
mBound = new Rect();
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
}
});
}
....
3、重寫onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 畫文本背景
mPaint.setColor(ContextCompat.getColor(mContext, R.color.default_color));
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
/**
* 獲取佈局寬高
*/
int width = getWidth();
int height = getHeight();
// 重新獲取文本大小
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
int boundWidth = mBound.width();
int boundHeight = mBound.height();
LogUtil.d(TAG + "--onDraw width=" + width + ",height=" + height);
LogUtil.d(TAG + "--onDraw boundWidth=" + boundWidth + ",boundHeight=" + boundHeight);
mPaint.setColor(mTextColor);
canvas.drawText(mTextContent, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
}
在xml中添加自定義的CustomTextView
<RelativeLayout 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">
<!--通過自定義字段調用自定義屬性,一定要在最外層佈局上添加 xmlns:app="http://schemas.android.com/apk/res-auto"-->
<com.evan.evanzchcustomview.view.CustomTextView
android:layout_width="300dp"
android:layout_height="200dp"
app:textColor="@color/colorPrimary"
app:textContent="1234"
app:textSize="34sp" />
</RelativeLayout>
這個時候查看好像沒問題,但是如果我們分別設置自定義View的寬高爲wrap_content
顯示不符合預期,明明設置的是wrap_content,結果卻是全屏顯示,原因是因爲系統幫我們測量的高度和寬度都是MATCH_PARNET,當我們設置明確的寬度和高度時,系統幫我們測量的結果就是我們設置的結果,當我們設置爲WRAP_CONTENT,或者MATCH_PARENT系統幫我們測量的結果就是MATCH_PARENT的長度。
所以,設置WRAP_CONTENT 要想正確顯示,就要重寫onMesure 方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
LogUtil.d(TAG + "--onMeasure textContent=" + mTextContent);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
// 如果模式爲EXACTLY、即指定特定確切的大小,寬度就爲設置的寬度
if (widthMode == MeasureSpec.EXACTLY) {
LogUtil.d(TAG + "--widthMode EXACTLY");
width = widthSize;
} else {
// 如果模式爲AT_MOST,如wrap_content,則寬度爲文本寬度+文本左右padding值
LogUtil.d(TAG + "--widthMode AT_MOST");
// 獲取文本的寬高
int paddingStart = getPaddingStart();
int paddingEnd = getPaddingEnd();
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
width = mBound.width() + paddingStart + paddingEnd;
}
// 高度通寬度一致
if (heightMode == MeasureSpec.EXACTLY) {
LogUtil.d(TAG + "--heightMode EXACTLY");
height = heightSize;
} else {
LogUtil.d(TAG + "--heightMode AT_MOST");
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
height = mBound.height() + paddingTop + paddingBottom;
}
setMeasuredDimension(width, height);
}
上面代碼涉及到 MeasureSpec 的三種模式
- UNSPECIFIED:不對View大小做限制,如:ListView,ScrollView
- EXACTLY:確切的大小,如:100dp或者march_parent
- AT_MOST:大小不可超過某數值,如:wrap_content
在運行程序,發現效果和預期一致
最後要實現點擊切換數字
在構造方法中設置監聽事件,點擊時切換文本,再通過postInvalidate刷新界面即可
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
randomText();
// Android的invalidate與postInvalidate都是用來刷新界面的。
//在UI主線程中,用invalidate();本質是調用View的onDraw()繪製。
//主線程之外,用postInvalidate()。
postInvalidate();
}
});
隨機生成四個數字
public void randomText() {
Set<Integer> integerSet = new HashSet<>();
Random random = new Random();
while (integerSet.size() < 4) {
int i = random.nextInt(10);
integerSet.add(i);
}
StringBuilder stringBuilder = new StringBuilder();
for (Integer num : integerSet) {
stringBuilder.append(num);
}
mTextContent = stringBuilder.toString();
}
最後就大功告成,實現文章開頭的那種效果,最後貼一下全部代碼
public class CustomTextView extends View {
private static final String TAG = "CustomView";
/**
* 文本內容
*/
private String mTextContent;
/**
* 文本顏色
*/
private int mTextColor;
/**
* 文本大小
*/
private float mTextSize;
public Context mContext;
private Rect mBound;
/**
* 畫筆
*/
private Paint mPaint;
/**
* 直接new一個Custom View 實例的時候,會調用第一個構造函數
*/
public CustomTextView(Context context) {
this(context, null);
}
/**
* 在xml佈局文件中調用Custom View的時候,會調用第二個構造函數
*/
public CustomTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
LogUtil.d(TAG + "--CustomView 2");
}
public CustomTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LogUtil.d(TAG + "--CustomView 3");
// 方式一
// TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomViewStyle, defStyleAttr, 0);
// 方式二
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomViewStyle);
mContext = context;
initView(context, typedArray);
}
public void initView(Context context, TypedArray typedArray) {
// 獲取設置屬性
mTextContent = typedArray.getString(R.styleable.CustomViewStyle_textContent);
mTextColor = typedArray.getColor(R.styleable.CustomViewStyle_textColor, ContextCompat.getColor(context, R.color.colorAccent));
mTextSize = typedArray.getDimension(R.styleable.CustomViewStyle_textSize, 15);
typedArray.recycle();
mPaint = new Paint();
mPaint.setTextSize(mTextSize);
mBound = new Rect();
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
randomText();
// Android的invalidate與postInvalidate都是用來刷新界面的。
//在UI主線程中,用invalidate();本質是調用View的onDraw()繪製。
//主線程之外,用postInvalidate()。
postInvalidate();
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(ContextCompat.getColor(mContext, R.color.default_color));
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
/**
* 獲取佈局寬高
*/
int width = getWidth();
int height = getHeight();
// 重新獲取文本大小
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
int boundWidth = mBound.width();
int boundHeight = mBound.height();
LogUtil.d(TAG + "--onDraw width=" + width + ",height=" + height);
LogUtil.d(TAG + "--onDraw boundWidth=" + boundWidth + ",boundHeight=" + boundHeight);
mPaint.setColor(mTextColor);
canvas.drawText(mTextContent, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
}
/**
* EXACTLY:一般是設置了明確的值或者是MATCH_PARENT
* AT_MOST:表示子佈局限制在一個最大值內,一般爲WARP_CONTENT
* UNSPECIFIED:表示子佈局想要多大就多大,很少使用
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
LogUtil.d(TAG + "--onMeasure textContent=" + mTextContent);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
LogUtil.d(TAG + "--widthMode EXACTLY");
width = widthSize;
} else {
LogUtil.d(TAG + "--widthMode AT_MOST");
// 獲取文本的寬高
int paddingStart = getPaddingStart();
int paddingEnd = getPaddingEnd();
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
width = mBound.width() + paddingStart + paddingEnd;
}
if (heightMode == MeasureSpec.EXACTLY) {
LogUtil.d(TAG + "--heightMode EXACTLY");
height = heightSize;
} else {
LogUtil.d(TAG + "--heightMode AT_MOST");
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
height = mBound.height() + paddingTop + paddingBottom;
}
setMeasuredDimension(width, height);
}
public void randomText() {
Set<Integer> integerSet = new HashSet<>();
Random random = new Random();
while (integerSet.size() < 4) {
int i = random.nextInt(10);
integerSet.add(i);
}
StringBuilder stringBuilder = new StringBuilder();
for (Integer num : integerSet) {
stringBuilder.append(num);
}
mTextContent = stringBuilder.toString();
}
}