本文原創,技術新手,如有問題,請多指教。
記得當初面試某公司的時候,面試官看我做Android的界面似乎做的很漂亮,有意将我引導做移動UI方面的工作,就問我關于有沒有自定義過元件,那時候的我,剛剛知道有自定義元件這個似乎跟高深的知識領域,但暈暈乎乎就被onMeasure onLayout等函數給弄糊塗了,更别說自己用過了,于是錯過了一個很好的機會。
機會永遠留給有準備的人。
當我接觸了更多的自定義View的資料,并且自己使用過後,自定義View其實沒有想象中那麼難。
我們為什麼要自定義控件?
1.為了特定的顯示風格,豐富界面。
2.使元件更夠處理特有的使用者互動。
3.可以優化界面的布局。
4.封裝特定功能的元件。
自定義元件的六大步驟。
- 自定義屬性的聲明和擷取。
- 測量onMeasure。
- 布局onLayout。
- 繪制onDraw。
- onTouchEvent。
- onInterceptTouchEvent。
其中這六大步驟中 3 和6 主要是用于自定義ViewGroup類型的元件的時候 用于子View的測量和定位的。
而1步驟主要是友善在xml中使用的。
是以最簡單的不需要包裝點選事件的一個自定義View其實隻需要2,4。
是不是很簡單?
下面我們詳細地介紹一下上面所屬的六大步驟。
自定義屬性的聲明與擷取
- 分析需要的自定義屬性
- 在res/values/attrs.xml中定義聲明。
- 在layout.xml檔案中進行使用。
- 在view的構造方法中進行擷取。
比如我需要的自定義元件是一個簡單的圓。
那麼我需要圓的半徑,圓的顔色等屬性。
res/values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--聲明自定義view的屬性 -->
<attr name="circle_radius" format="dimension"></attr>
<attr name="circle_color" format="color"></attr>
<declare-styleable name="MyCircleView">
<attr name="circle_radius" ></attr>
<attr name="circle_color" ></attr>
</declare-styleable>
</resources>
其中比較常用的format分别有string ,color,dimension,reference.等
分别對應 字元串 顔色 尺寸 引用的檔案 等格式。
自定義屬性結束之後,我們就可以建立MyCircleView繼承View了。
為了友善我們使用各種方式生成元件,我們需要實作三個構造函數都實作。
然後在構造函數中擷取我們聲明的屬性的值。
public class MyCircleView extends View {
private static final int DEFAULT_SIZE = ;
private static final int DEFAULT_COLOR = ;
private int mCircleColor;
private int mCirCleRadius;
private Paint mPaint;
// 構造函數 三個
public MyCircleView(Context context) {
this(context, null);
}
public MyCircleView(Context context, AttributeSet attrs) {
this(context, attrs, );
}
public MyCircleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 初始化 根據參數值初始化成員變量
obtainStyleAttrs(attrs);
}
// 添加參數 初始化參數
private void obtainStyleAttrs(AttributeSet attrs) {
TypedArray ta = getContext().obtainStyledAttributes(attrs,
R.styleable.MyCircleView);
// 擷取自定義屬性值
mCircleColor = ta.getColor(R.styleable.MyCircleView_circle_color,
DEFAULT_COLOR);
mCirCleRadius = dp2px((int) ta.getDimension(
R.styleable.MyCircleView_circle_radius, DEFAULT_SIZE));
ta.recycle();
}
// 機關轉換函數
// dp就是dip 裝置獨立像素 與密度(dpi)無關 與一個位于像素密度為 160dpi 的螢幕上的像素是一緻的
// px 像素 對應螢幕上的實際像素
// sp 與的dp類似 用于字型
// pt 螢幕實體長度機關 表示一個店 是螢幕的實體尺寸
// 可以用這樣一個簡單的公式: px=dp*(density/160)。舉個例子,在 密度 為 240 的螢幕上,1 個 DIP 等 于 1.5
// 個實體像素。
private int dp2px(int dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dpVal, getResources().getDisplayMetrics());
}
private int sp2px(int spVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
spVal, getResources().getDisplayMetrics());
}
關于螢幕機關的簡單介紹。
dp就是dip 裝置獨立像素 與密度(dpi)無關
與一個位于像素密度為 160dpi 的螢幕上的像素是一緻的
px 像素 對應螢幕上的實際像素
sp 與的dp類似 用于字型
pt 螢幕實體長度機關 表示一個點 是螢幕的實體尺寸
可以用這樣一個簡單的公式: px=dp*(density/160)。
舉個例子,在 密度 為 240 的螢幕上,1 個 DIP 等 于 1.5 個實體像素。
到此為止,我們已經完成了第一個大步驟的進行了。
現在我們需要了解onMeaure函數了。
首先我們要先了解一下測量的三種模式。
測量的三種模式
exactly 自定義的精确大小 固定不變的
at_most 根據内容的大小來決定整體的大小 最大不超過父控件的大小
unspecified 未規定
// 重寫函數
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 設定計算後的寬高
setMeasuredDimension(measureWidthSize(widthMeasureSpec),
measureHeightSize(heightMeasureSpec));
}
private int measureWidthSize(int widthMeasureSpec) {
// 擷取分别擷取寬的模式和尺寸
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int result = ;
if (widthMode == MeasureSpec.EXACTLY) {
// 測量規格模式:父元件已确定子元件的确切尺寸。 view要被賦予這些限制,無論它想要多大。
result = width;
} else {
// 根據内容确切地計算需要占用的尺寸
result = mCirCleRadius * + getPaddingLeft() + getPaddingRight();
if (widthMode == MeasureSpec.AT_MOST) {
// 測量規格模式:孩子可以達到指定尺寸的大小。
result = Math.min(width, result);
}
}
return result;
}
private int measureHeightSize(int heightMeasureSpec) {
// 擷取分别擷取寬的模式和尺寸
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int result = ;
if (heightMode == MeasureSpec.EXACTLY) {
// 測量規格模式:父元件已确定子元件的确切尺寸。 view要被賦予這些限制,無論它想要多大。
result = height;
} else {
// 根據内容确切地計算需要占用的尺寸
result = mCirCleRadius * + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.AT_MOST) {
// 測量規格模式:孩子可以達到指定尺寸的大小。
result = Math.min(height, result);
}
}
return result;
}
這裡使用到的MeasureSpec是 一個封裝類 用于提取mode 和 size 父控件向子空間傳遞的參數類型 用于表示子控件最大的size
Requestlayout()函數 用于重新測量
注意:OnMeasure() 方法比較經常被調用 最好不要将耗時操作放在這裡
關于onLayout()的使用 由于此次元件不是ViewGroup 我會放在後面介紹。
是以接下來就是onDraw()函數,即在我們的元件上畫一個圓。
這部分的内容需要大家了解一下畫布和畫筆的使用方法,這部分比較簡單,我不再贅述。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 重寫ondraw函數使用畫布canvas來繪制需要的圖形
// 設定畫筆屬性
mPaint = new Paint();
mPaint.setColor(mCircleColor);
canvas.drawCircle(getMeasuredWidth() / , getMeasuredHeight() / ,
mCirCleRadius, mPaint);
}
上面的代碼就是在元件的中間畫出指定大小和指定顔色的圓。
最後我也在代碼裡封裝了簡單的點選事件的封裝。
這樣更加利于代碼重用和功能封裝。
不需要在活動中另外實作監聽器重寫ontouch函數了。
不夠這樣的情況隻适用于你在任何情況都需要這樣的點選事件處理操作。
@Override
public boolean onTouchEvent(MotionEvent event) {
// 設定當使用者點選元件的時候 圓圈變大 直到圓圈直徑等于螢幕寬 圓圈縮小到預設大小。
//注意此動畫 不适用于元件自适應大小的時候。
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mCirCleRadius+=;
if(mCirCleRadius*>getMeasuredWidth())
{
mCirCleRadius=DEFAULT_SIZE;
}
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
break;
}
postInvalidate();
// 傳回true 表示已經消費了此事件
return true;
}
到此,我們的自定義元件已經大功告成了。
由于我們功能已經十分完善,我們隻需要在layout.xml中直接使用該元件,并指定半徑和顔色,就可以使用該元件,并且不需要在活動中寫任何代碼了。
活動的layout布局
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.example.myviewdemo.MyCircleView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#23232323"
app:circle_radius="100dp"
app:circle_color="#ffffffff"
/>
</LinearLayout>
我的活動中簡潔地就想沒寫過任何代碼一樣,但我們的應用以及實作了所有功能。
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
這就是自定義view的魅力之處。
