最近幾個月終於有大把時間總結這兩年來所學 2019.6.1
前言
上一篇自己寫了一個模仿微信的SlideView,主要複習了自定義View方面的一些知識點。這幾天有看到自定義ViewGroup相關的知識點,於是也打算拿一個簡單的自定義ViewGroup來學習一下。
TagLayout
效果展示
下面這個是一個個人興趣選擇界面,裏面主要是各種各樣的Tag。下面我選取其中一部分,來實現。
我的實現
模仿的對象
使用方法
先看看怎麼用
- 在xml中
...
<android.support.constraint.ConstraintLayout
...>
<com.cerkerli.library.view.TagLayout
android:id="@+id/tag_layout"
...>
<com.cerkerli.library.view.TagView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/material_design"
android:text="@string/material_design"
android:textSize="16sp"/>
<com.cerkerli.library.view.TagView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/rx_java"
android:text="@string/rxjava"
android:textSize="16sp"/>
<com.cerkerli.library.view.TagView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/gradle"
android:text="@string/gradle"
android:textSize="16sp"/>
<com.cerkerli.library.view.TagView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/android_studio"
android:text="@string/android_studio"
android:textSize="16sp"/>
<com.cerkerli.library.view.TagView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/kotlin"
android:text="@string/kotlin"
android:textSize="16sp"/>
<com.cerkerli.library.view.TagView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/flutter"
android:text="@string/flutter"
android:textSize="16sp"/>
<com.cerkerli.library.view.TagView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/more..."
android:text="@string/more"
android:textSize="16sp"/>
</com.cerkerli.library.view.TagLayout>
</android.support.constraint.ConstraintLayout>
在activity中
TagLayout tagLayout = findViewById(R.id.tag_layout);
tagLayout.setOnClickListener(new Listener.TagLayoutClickListener() {
@Override
public void onClick(int id, String text, boolean isSelected) {
switch (id){
case R.id.material_design:
case R.id.rx_java:
case R.id.gradle:
case R.id.android_studio:
case R.id.kotlin:
case R.id.flutter:
Util.toast(text + " " + isSelected);
break;
default:break;
}
}
});
我們可以在activity中做響應的操作。
在Application中,做初始化。
public void onCreate() {
super.onCreate();
Library.init(getApplicationContext());
}
--------------------------------我是分割線---------------------------------
如何實現
- 簡單介紹一下實現
TagView
TagView繼承自TextView,在TextView的基礎上添加了一個橢圓形的文本框。
重點方法有三個:onMeasure,onDraw,onTouchEvent
,下面分別介紹一個每個方法。
onMeasure
onMeasure的主要功能是重新測量View的尺寸。我們知道在xml的配置中,layout_width,layout_height
有三種值,分別是wrap_content,match_parent,exactly「10dp 30dp」
,我在onMeasure中做了下面的處理:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//測量文字長度
getPaint().getTextBounds(getText().toString(), 0, getText().toString().length(), textRect);
widthSize = textRect.right - textRect.left;
//測量文字的推薦高度
//這個問題被卡的比較久,之前一個用的是 textRect的top和bottom
Paint.FontMetrics fontMetrics = getPaint().getFontMetrics();
heightSize = (int)(fontMetrics.bottom - fontMetrics.top);
//重新測量長和寬
setMeasuredDimension(widthSize + heightSize * 2,
heightSize + heightSize);
}
這裏可以看到,我強制重新測量了文字的width和height,改成了wrap_content。這意味着,整個文本框的大小都由文字的大小決定,而不是 layout_width,layout_height
這兩個值,就是說,不管你把layout_width,layout_height
寫成多少dp,最後整個文本框的大小都由 textSize
參數決定。
這裏的重要知識點是文字的測量,想要具體學習的話,推薦看看Hencoder課程
onDraw
onDraw的功能比較單一了,繪製一個文本框,以及對文字做一個偏移,保證文字居中對齊於文本框。實現的效果像下面這樣子。
代碼如下:
@Override
protected void onDraw(Canvas canvas) {
canvas.drawRoundRect(CIRCLE_WIDTH, CIRCLE_WIDTH,
getWidth() - (CIRCLE_WIDTH << 1),
getHeight() - (CIRCLE_WIDTH << 1), circleRadius, circleRadius, mPaint);
canvas.save();
canvas.translate(heightSize, heightSize >> 1);
super.onDraw(canvas);
canvas.restore();
}
onTouchEvent
onTouchEvent 實現點擊事件,由於這個點擊事件比較簡單,所以就沒有用框架,自己簡單實現了一下。
點擊事件重點是兩步:捕獲事件,消費事件
捕獲事件在MotionEvent.ACTION_DOWN中返回true即可。
消費事件就是在其他動作裏面做一些操作。
代碼如下:
public boolean onTouchEvent(MotionEvent event) {
//捕獲事件
if(event.getActionMasked() == MotionEvent.ACTION_DOWN){
return true;
}
//消費事件
if(event.getActionMasked() == MotionEvent.ACTION_UP){
//未點擊-->點擊
if(!isSelected){
isSelected = true;
setPaintSelected(true);
}else {
//點擊-->未點擊
isSelected = false;
setPaintSelected(false);
}
//監聽回調
if(clickListener != null){
clickListener.onClick(getText().toString(),isSelected);
}
invalidate();
}
return false;
}
- TagView的部分是比較簡單的,我在實現的時候,主要是遇到了文字測量的問題,花了不少時間,其他的都很基礎。
TagLayout
TagLayout分爲兩部分,onMeasure和onLayout
onLayout
onLayout 比較容易,分別繪製每一個子View,然後將onMeasure中存儲的數據讀取出來就可以了,代碼如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for(int i = 0;i < getChildCount();i++){
final TagView child = (TagView)getChildAt(i);
Rect childBond = mChildBounds.get(i);
child.layout(childBond.left,childBond.top,childBond.right,childBond.bottom);
//設置點擊監聽
child.setOnClickListener(new Listener.TagViewClickListener() {
@Override
public void onClick(String text,boolean isSelected) {
if(clickListener != null){
clickListener.onClick(child.getId(),text,isSelected);
}
}
});
}
}
監聽的部分是監聽子View中的點擊事件。
onMeasure
- onMeasure的實現非常非常麻煩,麻煩在於需要挨個測量子View的尺寸,以及計算自己的尺寸,還有各種換行的情況需要考慮,但是整體的邏輯是很簡單的,先貼一下代碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//當前行高
int LineWidthUsed = 0;
int LineHeightUsed = 0;
//總行高
int HeightUsed = 0;
int WidthUsed = 0;
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
for(int i = 0;i < getChildCount();i++){
//獲取子 View 以及佈局子 View
View child = getChildAt(i);
measureChildWithMargins(child,widthMeasureSpec,LineWidthUsed,heightMeasureSpec,HeightUsed);
if(LineWidthUsed + child.getMeasuredWidth() + Constants.WIDTH_NAP > widthSize &&
widthMode != MeasureSpec.UNSPECIFIED){
//是否需要換行
WidthUsed = LineWidthUsed;
LineWidthUsed = 0;
HeightUsed += LineHeightUsed;
HeightUsed += Constants.HEIGHT_NAP;
measureChildWithMargins(child,widthMeasureSpec,LineWidthUsed,heightMeasureSpec,HeightUsed);
LLog.d(TAG,"換行 at " + i + " WidthUsed " + WidthUsed + " HeightUsed " + HeightUsed);
}
//數據寫入List中 供Layout使用
mRect = new Rect();
mRect.set(LineWidthUsed,HeightUsed,LineWidthUsed + child.getMeasuredWidth(),HeightUsed + child.getMeasuredHeight());
mChildBounds.add(mRect);
LLog.d(TAG,"i == "+i);
LLog.d(TAG,mRect);
//數據增加
LineWidthUsed += child.getMeasuredWidth();
LineWidthUsed += Constants.WIDTH_NAP;
LineHeightUsed = Math.max(LineHeightUsed,child.getMeasuredHeight());
}
HeightUsed+=LineHeightUsed;
setMeasuredDimension(widthSize, HeightUsed);
LLog.d(TAG,"setMeasuredDimension "+ widthSize + ":" + HeightUsed);
}
這個整體邏輯其實很簡單,就是挨個去測量每個子View的尺寸,然後記錄下來,最後把自己的總尺寸也記錄下來。
這裏有三個很關鍵的方法:measureChildWithMargins
和child.getMeasuredWidth() child.getMeasuredHeight()
和setMeasuredDimension(...);
。
方法1. measureChildWithMargins
:這個方法是獲取LayoutParams
,然後調用child.measure(...)
,我可以貼一下他的源碼,
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
從源碼可以看出,這個其實是一個集成好的方法,比較方便我們寫的時候,就不用在一一判斷三種值了(上面說到了wrap_content,match_parent,exactly「10dp 30dp」
)
方法2. child.getMeasuredWidth() child.getMeasuredHeight()
:這兩個是獲取子View的寬高,方便我們計算子View,然後通過子View計算自己的寬高。
方法3. setMeasuredDimension(...);
:這個方法就是確定自己的寬高,和View的中用法一模一樣。
— 這裏就簡單介紹一下TagLayout的實現,需要有一些基礎才能看懂,所以沒有基礎的,建議看看Hencoder課程,這個我目前見過講自定義View最完善的課程,沒有之一。
再另外,以上都是自己平時所學整理,如果有錯誤,歡迎留言或者添加微信批評指出,一起學習,共同進步,愛生活,愛技術。