版權聲明:本文爲openXu原創文章【openXu的博客】,未經博主允許不得以任何形式轉載
目錄:
這篇博客我們來一發自定義控件的實戰,恰好前些天有一個小需求,效果圖如下:
根據效果圖,我們可以確定,用自定義View完全可以搞定,在自定義控件系列博客第一篇中,我們總結了自定義View的幾個步驟:
- 繼承View,覆蓋構造方法
- 自定義屬性
- 重寫onMeasure方法測量寬高
- 重寫onDraw方法繪製控件
當然,你沒有必要完全依照步驟去做,這個步驟是你對控件應該怎麼寫已經有了完整的思路和規劃,這在實際情況下是不現實的,往往我們自定義控件都是做到哪裏缺什麼就做什麼,首先我們應該將它畫出來,有一個可視的供我們思考的視圖。所以,這裏我們將這個步驟靈活的變換一下,由於我們現在還不確定需要自定義哪些屬性,以及需要怎樣測量,所以我們把這兩個步驟挪到後面。
1. 初步分析,重寫onDraw繪製
首先我們分析一下這個控件裏面有哪些元素,有一條直線,上面有n個選項,分佈着n個圓,當選中哪一個後這上面的圓變爲藍色的,還有n項字,當選中後字變爲藍色。下面我們初步確定一下需要的常量和一些簡單的計算:
- 一個供選擇的數組
String[] tabNames = new String[]{"tab1","tab2","tab3","tab4"}
- 一些必要的數據:字體大小
mTextSize
,字體顏色mColorTextDef
,線段和圓圈的顏色mColorDef
,被選中後的顏色mColorSelected
,直線的高度mLineHight
,圓圈的直徑mCircleHight
,被選中後藍色空心圓圈的寬度mCircleSelStroke
,當前選中的序號selectedIndex
- 直線的長度
float lineLength
=整個控件的寬度-左邊圓圈的半徑 -右邊圓圈的半徑(爲了讓直線兩端正好在兩端圓圈的中心) - 圓圈的分佈間隔距離
float splitLength = lineLength / (n-1);
- 字體與上面部分的間距
mMarginTop
在動手之前,我們要注意:直線的長度應該在控件完成測量後才能計算,所以應該在onMeasure
中計算。現在我們可以動手了,首先繼承View
,覆蓋構造方法,然後重寫onDraw
,在上面畫出初步的輪廓。
代碼:
public class SlideTab extends View {
String TAG = "SlidingTab";
private int mTextSize; //文本的字體大小
private int mColorTextDef; // 默認文本的顏色
private int mColorDef; // 線段和圓圈顏色
private int mColorSelected; //選中的字體和圓圈顏色
private int mLineHight; //基準線高度
private int mCircleHight; //圓圈的高度(直徑)
private int mCircleSelStroke; //被選中圓圈(空心)的粗細
private int mMarginTop; //圓圈和文字之間的距離
private String[] tabNames; //需要繪製的文字
/**
* 下面需要計算
*/
private float splitLengh; //每一段橫線長度
private int textStartY; //文本繪製的Y軸座標
private List<Rect> mBounds; //保存文本的量的結果
private int selectedIndex = 0; //當前選中序號
private Paint mTextPaint; //繪製文字的畫筆
private Paint mLinePaint; //繪製基準線的畫筆
private Paint mCirclePaint; //繪製基準線上灰色圓圈的畫筆
private Paint mCircleSelPaint; //繪製被選中位置的藍色圓圈的畫筆
public SlideTab(Context context) {
this(context, null);
}
public SlideTab(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideTab(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化屬性
tabNames = new String[]{"tab1","tab2","tab3","tab4"};
mColorTextDef = Color.GRAY;
mColorSelected = Color.BLUE;
mColorDef = Color.argb(255,234,234,234); //#EAEAEA
mTextSize = 20;
mLineHight = 5;
mCircleHight = 20;
mCircleSelStroke = 10;
mMarginTop = 50;
mLinePaint = new Paint();
mCirclePaint = new Paint();
mTextPaint = new Paint();
mCircleSelPaint = new Paint();
mLinePaint.setColor(mColorDef);
mLinePaint.setStyle(Paint.Style.FILL);//設置填充
mLinePaint.setStrokeWidth(mLineHight);//筆寬像素
mLinePaint.setAntiAlias(true);//鋸齒不顯示
mCirclePaint.setColor(mColorDef);
mCirclePaint.setStyle(Paint.Style.FILL);//設置填充
mCirclePaint.setStrokeWidth(1);//筆寬像素
mCirclePaint.setAntiAlias(true);//鋸齒不顯示
mCircleSelPaint.setColor(mColorSelected);
mCircleSelPaint.setStyle(Paint.Style.STROKE); //空心圓圈
mCircleSelPaint.setStrokeWidth(mCircleSelStroke);
mCircleSelPaint.setAntiAlias(true);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mColorTextDef);
mLinePaint.setAntiAlias(true);
measureText();
}
/**
* measure the text bounds by paint
*/
private void measureText(){
mBounds = new ArrayList<>();
for(String name : tabNames){
Rect mBound = new Rect();
mTextPaint.getTextBounds(name, 0, name.length(), mBound);
mBounds.add(mBound);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
initConstant();
}
private void initConstant(){
int lineLengh = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleHight;
splitLengh = lineLengh/(tabNames.length-1);
textStartY = mCircleHight + mMarginTop + getPaddingTop();
}
@Override
protected void onDraw(Canvas canvas) {
//畫灰色基準線
canvas.drawLine(mCircleHight/2, mCircleHight/2, getWidth()-mCircleHight/2,mCircleHight/2 , mLinePaint);
float centerY = mCircleHight/2;
for(int i = 0; i<tabNames.length; i++){
float centerX = mCircleHight/2+(i*splitLengh);
//float cx, float cy, float radius, @NonNull Paint paint
//畫基準線上灰色小圓圈
// Log.v(TAG, "畫圓:X:"+centerX+" Y:"+centerY);
canvas.drawCircle(centerX, centerY,mCircleHight/2,mCirclePaint);
mTextPaint.setColor(mColorTextDef);
if(selectedIndex == i){
//畫選中位置的藍色圓圈
mCircleSelPaint.setStrokeWidth(mCircleSelStroke);
mCircleSelPaint.setStyle(Paint.Style.STROKE);
// Log.v(TAG, "畫圓:X:"+centerX+" Y:"+centerY+" 半徑:"+(mCircleHight-mCircleSelHight)/2);
canvas.drawCircle(centerX, centerY, (mCircleHight-mCircleSelStroke)/2, mCircleSelPaint);
mTextPaint.setColor(mColorSelected);
}
//繪製文字
float startX;
if(i == 0){
startX = 0;
}else if(i == tabNames.length-1){
startX = getWidth()-mBounds.get(i).width();
}else{
startX = centerX-(mBounds.get(i).width()/2);
}
// Log.v(TAG, "寫字:X:"+startX+" Y:"+textStartY +" 字寬度:"+mBounds.get(i).width());
canvas.drawText(tabNames[i], startX, textStartY, mTextPaint);
}
}
}
佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dip">
<com.openxu.st.SlideTab
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#aaff0000"/>
</LinearLayout>
運行效果:
2. 重寫onMeasure計算寬高
基本的效果圖已經出來了,不知道你們有沒有發現,我在寫佈局文件的時候設置的高度是wrap_content
,並且爲控件設置了紅色背景以便於參考,運行結果顯示控件的高度卻佔滿的整個屏幕,所以我們應該用重寫onMeasure
測量控件的高度(不熟悉onMeasure
可以參照博客Android自定義View(三、深入解析控件測量onMeasure))。對於此控件,它的高度設置爲填充父窗體,高度應該是圓圈的直徑+字體的高度+字體與上面部分的距離。
重寫onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec); //獲取寬的模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec); //獲取高的模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //獲取寬的尺寸
int heightSize = MeasureSpec.getSize(heightMeasureSpec); //獲取高的尺寸
int height ;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
float textHeight = mBounds.get(0).height();
height = (int) (textHeight + mCircleHight + mMarginTop);
// Log.v(TAG, "文本的高度:"+textHeight + "控件的高度:"+height);
}
//保存測量寬度和測量高度
setMeasuredDimension(widthSize, height);
initConstant();
}
運行結果:
發現高度還是不對,其實這個地方並不是上面重寫onMeasure
有問題,而是繪製文本的Y座標的問題,我們看看drawText
方法的註釋:
/**
* Draw the text, with origin at (x,y), using the specified paint. The
* origin is interpreted based on the Align setting in the paint.
*
* @param text The text to be drawn
* @param x The x-coordinate of the origin of the text being drawn
* @param y The y-coordinate of the baseline of the text being drawn
* @param paint The paint used for the text (e.g. color, size, style)
*/
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
native_drawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags,
paint.getNativeInstance(), paint.mNativeTypeface);
}
對於參數y的說明中,它指的是baseline的y軸座標,而不是文字top的y座標,對於baseline,後面再做說明,所以,我們計算textStartY
的時候,應該計算baseline的y座標:
private void initConstant(){
int lineLengh = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleHight;
splitLengh = lineLengh/(tabNames.length-1);
// FontMetrics對象
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
textStartY = getHeight() - (int)fontMetrics.bottom; //baseLine的位置
// textStartY = mCircleHight + mMarginTop + getPaddingTop();
}
再看看運行效果:
3. 重寫onTouch加入滑動效果
現在,文字顯示已經沒有問題了,接下來,我們加入手指滑動的效果。此控件只支持左右滑動,手指滑動到某個位置的時候記錄xy的座標值,然後將藍色選中的圓圈移動到x位置,其實就是在手指的位置畫一個藍色的圓圈,還要根據x的值計算當前偏向於選擇哪一個標籤。這裏需要注意的地方是event.getX()
和event.getY()
獲取到的手指的座標是相對於本控件左上角的座標(本控件左上角爲原點),具體看下面代碼,註釋已經很清楚了:
@Override
protected void onDraw(Canvas canvas) {
//畫灰色基準線
canvas.drawLine(mCircleHight/2, mCircleHight/2, getWidth()-mCircleHight/2,mCircleHight/2 , mLinePaint);
float centerY = mCircleHight/2;
for(int i = 0; i<tabNames.length; i++){
float centerX = mCircleHight/2+(i*splitLengh);
//float cx, float cy, float radius, @NonNull Paint paint
//畫基準線上灰色小圓圈
// Log.v(TAG, "畫圓:X:"+centerX+" Y:"+centerY);
canvas.drawCircle(centerX, centerY,mCircleHight/2,mCirclePaint);
mTextPaint.setColor(mColorTextDef);
if(selectedIndex == i){
if(!isSliding){
//畫選中位置的藍色圓圈
mCircleSelPaint.setStrokeWidth(mCircleSelStroke);
mCircleSelPaint.setStyle(Paint.Style.STROKE);
// Log.v(TAG, "畫圓:X:"+centerX+" Y:"+centerY+" 半徑:"+(mCircleHight-mCircleSelHight)/2);
canvas.drawCircle(centerX, centerY, (mCircleHight-mCircleSelStroke)/2, mCircleSelPaint);
}
mTextPaint.setColor(mColorSelected);
}
//繪製文字
float startX;
if(i == 0){
startX = 0;
}else if(i == tabNames.length-1){
startX = getWidth()-mBounds.get(i).width();
}else{
startX = centerX-(mBounds.get(i).width()/2);
}
// Log.v(TAG, "寫字:X:"+startX+" Y:"+textStartY +" 字寬度:"+mBounds.get(i).width());
canvas.drawText(tabNames[i], startX, textStartY, mTextPaint);
}
//畫手指拖動位置圓圈,最後畫,避免被其他圓圈覆蓋
if(isSliding){
// Log.v(TAG, "手指拖動畫圓:X:"+slidX+" Y:"+centerY+" 半徑:"+mCircleHight/2);
mCircleSelPaint.setStrokeWidth(1);
mCircleSelPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(slidX, centerY, mCircleHight/2, mCircleSelPaint);
}
}
private boolean isSliding = false; //手指是否在拖動
private float slidX, slidY; //手指當前位置(相對於本控件左上角的座標)
@Override
public boolean onTouchEvent(MotionEvent event) {
slidX = event.getX(); //以本控件左上角爲座標原點
slidY = event.getY();
//左右越界
if(slidX< mCircleHight/2)
slidX = mCircleHight/2;
if(slidX>(getWidth() - mCircleHight/2))
slidX = getWidth() - mCircleHight/2;
Log.e(TAG, "手指位置: getX:"+slidX+" getY:"+slidY);
float select = slidX/splitLengh;
int xs = (int)(select*10)-(((int)select)*10);
selectedIndex = (int)select +(xs>5?1:0);
// Log.w(TAG, "手指位置在第"+select+"位置,小數爲:"+xs+" ,選中的序列爲:"+selectedIndex);
//TODO 如果要求手指脫離了直線所在矩形之後停止滑動,放開下面代碼
/* if(slidY>mCircleHight || slidY < 0){
Log.e(TAG, "手指落在外面了");
if(isSliding){ //滑動到外面的,這時候需要重新繪製一次,其他事件不用重繪
isSliding = false;
invalidate();
}
isSliding = false;
return super.onTouchEvent(event);
}*/
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
isSliding = true;
// Log.e(TAG, "手指按下: getX:"+slidX+" getY:"+slidY);
break;
case MotionEvent.ACTION_MOVE:
// Log.i(TAG, "手指滑動: getX:"+slidX+" getY:"+slidY);
break;
case MotionEvent.ACTION_UP:
// Log.e(TAG, "手指擡起: getX:"+slidX+" getY:"+slidY);
isSliding = false;
break;
}
invalidate();
return true;
}
效果圖:
4. 自定義屬性
目前爲止,控件基本能夠正常使用了,如果你認爲這樣就可以了,那就不用往下看了。這個樣子使用起來很不方便,如果很多地方需要用到此控件,而且控件中的字體大小顏色等都不一樣,那是不是得寫很多這樣的控件(只是改變一下里面一些常量的值)?所以爲了讓這個控件使用更加靈活,可以自定義一些屬性,這樣只需要在佈局文件中設置屬性值即可。自定義屬性具體方法請參見(Android自定義View(二、深入解析自定義屬性))。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
private int mColorTextDef; // 默認文本的顏色
private int mColorDef; // 線段和圓圈顏色
private int mColorSelected; //選中的字體和圓圈顏色
private int mLineHight; //基準線高度
private int mCircleHight; //圓圈的高度(直徑)
private int mCircleSelStroke; //被選中圓圈(空心)的粗細
private int mMarginTop; //圓圈和文字之間的距離
private String[] tabNames; //需要繪製的文字
private int mTextSize; //文本的字體大小
-->
<declare-styleable name="SlidTab">
<attr name="textColorDef" format="reference|color"/> <!--默認文本的顏色-->
<attr name="android:textSize"/> <!--文本的字體大小-->
<attr name="defColor" format="reference|color" /> <!--線段和圓圈顏色-->
<attr name="selectedColor" format="reference|color" /><!--選中的字體和圓圈顏色-->
<attr name="lintHight" format="dimension" /> <!--基準線高度-->
<attr name="circleHight" format="dimension" /> <!--圓圈的高度(直徑)-->
<attr name="circleSelStroke" format="dimension" /> <!--被選中圓圈(空心)的粗細-->
<attr name="mMarginTop" format="dimension" /> <!--圓圈和文字之間的距離-->
<attr name="tabNames" format="reference" /> <!--需要繪製的文字-->
</declare-styleable>
</resources>
佈局中使用:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:openXu="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dip">
<com.openxu.st.SlideTab
android:id="@+id/slideTab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize = "15sp"
openXu:textColorDef = "#A4A4A4"
openXu:defColor = "#EAEAEA"
openXu:selectedColor = "#5CBB8C"
openXu:lintHight = "2dip"
openXu:circleHight = "20dip"
openXu:circleSelStroke = "5dip"
openXu:mMarginTop = "15dip"
openXu:tabNames = "@array/tab_names" />
</LinearLayout>
運行效果:
歡迎關注,希望在這裏有你想要的,博主會持續更新高(di)質(ji)量(shu)的文章和大家交流學習,祝各位學習愉快。
喜歡請點贊,no愛請勿噴~O(∩_∩)O謝謝
##源碼下載:
注:沒有積分的童鞋 請留言索要代碼喔
http://download.csdn.net/detail/u010163442/9698879 CSDN下載平臺太流氓
https://github.com/openXu/SlidingTab