android面试自定义view,资深面试官:自定义View的实现方式,你知道几种?
前提
為什么要自定義View?
怎么自定義View?
當 Android SDK 中提供的系統 UI 控件無法滿足業務需求時,我們就需要考慮自己實現 UI 控件。而且自定義View在面試時是很經典的一道Android面試題,對其的理解程度決定面試官看你的高度。
PS:關于我
本人是一個擁有6年開發經驗的帥氣Android工程師,記得看完點贊,養成習慣,微信搜一搜「 程序猿養成中心 」關注這個喜歡寫干貨的程序員。
另外耗時兩年整理收集的Android一線大廠面試完整考點PDF出爐,資料【完整版】已更新在我的【Github】,有面試需要、或者想梳理自己的Android知識體系的朋友們可以去參考參考,如果對你有幫助,可以點個Star哦!
自定義View的方式
繼承系統提供的成熟控件(比如 LinearLayout、RelativeLayout、ImageView 等);
直接繼承自系統 View 或者 ViewGroup,并自繪顯示內容。
建議:盡量直接使用系統提供的UI控件、或者方式1實現需求效果,因為google提供的UI控件等都已經做了非常完整的邊界、特殊case處理。自定義的View可能由于考慮不周全在適配,某些特殊case下出現問題等。
方式1: 繼承系統UI控件
舉例:繼承RelativeLayout實現一個titleBar
1 添加布局
注意:這里使用merge標簽,因為后面我們extends RelativeLayout,不需要再套一層
xmlns:tool="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:id="@+id/left_button"
android:layout_width="40dp"
android:layout_height="56dp"
android:layout_marginStart="6dp"
android:paddingStart="5dp"
android:paddingTop="13dp"
android:paddingEnd="5dp"
android:paddingBottom="13dp"/>
android:id="@+id/title_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_toStartOf="@+id/right_button"
android:layout_toEndOf="@id/left_button"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textColor="@color/color_ffffff"
android:textSize="18sp"
tool:text="This is tools text"/>
android:id="@+id/right_button"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dp"
android:layout_marginEnd="6dp"
android:padding="5dp"/>
2 添加自定義屬性
在valules的attrs.xml中定義如下自定義屬性
name : 是屬性名稱
format : 代表屬性的格式
使用自定義屬性的時候需要添加命名空間如:xmlns:app(可以自己定義)
3 TitleBar代碼實現
public class TitleBarLayout extends RelativeLayout {
public TitleBarLayout(Context context) {
this(context, null);
}
public TitleBarLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TitleBarLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 引入步驟1中添加的布局
ViewUtils.inflate(this, R.layout.layout_title_bar, true);
// 獲取自定義屬性
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.TitleBar);
Drawable leftDrawable = typedArray.getDrawable(R.styleable.TitleBar_title_left_icon);
Drawable rightDrawable = typedArray.getDrawable(R.styleable.TitleBar_title_right_icon);
CharSequence titleText = typedArray.getString(R.styleable.TitleBar_title_bar_title_text);
int titleTextColor = typedArray.getColor(R.styleable.TitleBar_title_bar_text_color, Color.BLACK);
// 如果不調用recycle,As會有提示
typedArray.recycle();
ImageView leftButton = findViewById(R.id.left_button);
ImageView rightButton = findViewById(R.id.right_button);
TextView titleTextView = findViewById(R.id.title_view);
if (leftDrawable != null) {
leftButton.setImageDrawable(leftDrawable);
}
if (rightDrawable != null) {
rightButton.setImageDrawable(rightDrawable);
}
titleTextView.setText(titleText);
titleTextView.setTextColor(titleTextColor);
}
}
方式2: 繼承View / ViewGroup
自定義View的時候,一般是3步走:
1 重寫onMeasure
測量子View及View本身的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
為什么我們需要重寫onMeasure?
如果我們直接在 XML 布局文件中定義好 View 的寬高,然后讓自定義 View 在此寬高的區域內顯示即可,那么就不需要重寫onMeasure了。
但是Android 系統提供了 wrap_content 和 match_parent 屬性來規范控件的顯示規則。它們分別代表自適應大小和填充父視圖的大小,但是這兩個屬性并沒有指定具體的大小,因此我們需要在 onMeasure 方法中過濾出這兩種情況,真正的測量出自定義 View 應該顯示的寬高大小.
MesureSpec
MesureSpec 定義 :
測量規格,View根據該規格從而決定自己的大小。
MeasureSpec是由一個32位 int 值來表示的。其中該 int 值對應的二進制的高2位代表SpecMode,低30位代表SpecSize
測量模式
EXACTLY:表示在 XML 布局文件中寬高使用 match_parent 或者固定大小的寬高;
AT_MOST:表示在 XML 布局文件中寬高使用 wrap_content;
UNSPECIFIED:父容器沒有對當前 View 有任何限制,當前 View 可以取任意尺寸,比如 ListView 中的 item。
自定義FlowLayout onMeasure方法說明(看注釋 !看助手!看注釋!)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 寬度測量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 高度測量模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 測量寬度
int width = MeasureSpec.getSize(widthMeasureSpec);
// 測量高度
int height = MeasureSpec.getSize(heightMeasureSpec);
// 每一行的寬度(FlowLayout當標簽長度超過一行后會換行)
int lineWidth = 0;
// 最終測量的width
int resultWidth = 0;
// 最終測量的height
int resultHeight = 0;
// 換行次數
int lineCount = 1;
// 遍歷所有子View,拿到每一行的最大寬度 和 換行次數
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
measureChild(childAt, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
lineWidth += childAt.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
// 換行
if (lineWidth > width) {
resultWidth = Math.max(lineWidth - childAt.getMeasuredWidth(), resultWidth);
lineWidth = 0;
lineCount++;
}
}
View lastChild = getChildAt(getChildCount() - 1);
MarginLayoutParams marginParams = (MarginLayoutParams) lastChild.getLayoutParams();
// 根據換行次數 + 2(第一行和最后一行) * 高度,算出最終的高度
resultHeight += (lastChild.getMeasuredHeight() + marginParams.topMargin + marginParams.bottomMargin) * (lineCount + 2);
resultWidth += getPaddingLeft() + getPaddingRight();
resultHeight += getPaddingTop() + getPaddingBottom();
// 重寫onMeasure必須調用這個方法來保存測量的寬、高
setMeasuredDimension(widthMode == MeasureSpec.AT_MOST ? resultWidth : width,
heightMode == MeasureSpec.AT_MOST ? resultHeight : height);
}
2 重寫onDraw
繪制自身內容
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
onDraw 方法接收一個 Canvas 類型的參數。Canvas 可以理解為一個畫布,在這塊畫布上可以繪制各種類型的 UI 元素。
Canvas 中每一個繪制操作都需要傳入一個 Paint 對象。Paint 就相當于一個畫筆,我們可以通過設置畫筆的各種屬性。
如果不想看Canvas、Paint枯燥的文檔,可以搜索Hencoder,看下hencoder大佬的自定義教程,有趣生動。
3、 重寫onLayout
擺放子View位置(繼承ViewGroup必須要重寫)
自定義FlowLayout onLayout方法說明(看注釋 !看注釋!看注釋!)
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 獲取FlowLayout 寬度
int parentWidth = getMeasuredWidth();
// 子View擺放的位置
int left, top, right, bottom;
// 一行的寬度
int lineWidth = 0;
// 一行的高度
int lineHeight = 0;
// 遍歷子View 計算它們應該擺放的位置
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
int childWidth = childAt.getMeasuredWidth();
int childHeight = childAt.getMeasuredHeight();
MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
left = lineWidth + layoutParams.leftMargin;
right = left + childWidth + layoutParams.rightMargin;
top = lineHeight + layoutParams.topMargin;
bottom = top + childHeight + layoutParams.bottomMargin;
if (right > parentWidth) {
lineWidth = 0;
lineHeight += childHeight + layoutParams.topMargin + layoutParams.bottomMargin;
left = lineWidth + layoutParams.leftMargin;
right = left + childWidth + layoutParams.rightMargin;
top = lineHeight + layoutParams.topMargin;
bottom = top + childHeight + layoutParams.bottomMargin;
}
childAt.layout(left, top, right, bottom);
lineWidth += childWidth + layoutParams.rightMargin + layoutParams.leftMargin;
}
}
效果圖如下
在這里插入圖片描述
FlowLayout完成代碼
public class FlowLayout extends ViewGroup {
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int lineWidth = 0;
int resultWidth = 0;
int resultHeight = 0;
int lineCount = 1;
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
measureChild(childAt, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
lineWidth += childAt.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
if (lineWidth > width) {
resultWidth = Math.max(lineWidth - childAt.getMeasuredWidth(), resultWidth);
// 換行
lineWidth = 0;
lineCount++;
}
}
View lastChild = getChildAt(getChildCount() - 1);
MarginLayoutParams marginParams = (MarginLayoutParams) lastChild.getLayoutParams();
resultHeight += (lastChild.getMeasuredHeight() + marginParams.topMargin + marginParams.bottomMargin) * (lineCount + 2);
resultWidth += getPaddingLeft() + getPaddingRight();
resultHeight += getPaddingTop() + getPaddingBottom();
setMeasuredDimension(widthMode == MeasureSpec.AT_MOST ? resultWidth : width,
heightMode == MeasureSpec.AT_MOST ? resultHeight : height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int parentWidth = getMeasuredWidth();
int left, top, right, bottom;
int lineWidth = 0;
int lineHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
int childWidth = childAt.getMeasuredWidth();
int childHeight = childAt.getMeasuredHeight();
MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
left = lineWidth + layoutParams.leftMargin;
right = left + childWidth + layoutParams.rightMargin;
top = lineHeight + layoutParams.topMargin;
bottom = top + childHeight + layoutParams.bottomMargin;
if (right > parentWidth) {
lineWidth = 0;
lineHeight += childHeight + layoutParams.topMargin + layoutParams.bottomMargin;
left = lineWidth + layoutParams.leftMargin;
right = left + childWidth + layoutParams.rightMargin;
top = lineHeight + layoutParams.topMargin;
bottom = top + childHeight + layoutParams.bottomMargin;
}
childAt.layout(left, top, right, bottom);
lineWidth += childWidth + layoutParams.rightMargin + layoutParams.leftMargin;
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
}
總結
以上是生活随笔為你收集整理的android面试自定义view,资深面试官:自定义View的实现方式,你知道几种?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 黑色沙漠加贝尔在哪
- 下一篇: 阿里 框架 原声Android,阿里P8