編輯:關於Android編程
我要自定義的控件是一個蓋世英雄,
它不僅僅是一個Loading控件,同時還支持進度條 (ProgressBar)功能 。
它會在你需要的時候出現,
它支持 left,top,right,bottom 四個方向加載(變色),最重要的是,它可以是 文字,也可以是 圖片,能夠滿足開發者一切需求。
如果你想用它來做LoadingView(圖片文字都可以,下面用圖片演示)
這是你想要的效果嗎?限制太死?不會!你可以:
設置重新從底部加載,而不是從頂部折返 設置動畫時間 設置是否重復執行如果你想做進度條ProgressBar(圖片文字都可以,下面用文字演示)
怎麼樣?看完效果是不是覺得還不錯呢?
那麼這樣一個實用又酷炫的自定義控件到底有多難呢?
代碼寫到一半的時候我忽然理解了鴻洋的那個32秒。這樣的控件,簡單到爆。
原理:其實就是使用兩種不同的顏色繪制兩遍文字,通過裁剪畫布控制兩種顏色的展示
那麼重點就是一個方法啦,裁剪畫布 canvas.clipRect。知道這個方法的人簡直小菜一碟。難怪鴻洋32秒搞定。
不過有一點還是值得注意的:繪制居中文字
凡事由簡單到復雜。先實現文字類型的功能,之後再加一個圖片功能,就幾行代碼的事情。
為什麼說文字居中需要注意呢?以前學習的第一個自定義View應該就是從繪制文字開始,但是大家可能都沒有注意到,網上使用的方式繪制居中文字是有問題的。在寬高設置為 wrap_content,並且不設置 padding 的情況下,文本是不能完整繪制的。
博客中詳細的描述,測試,對比了文本居中繪制的各種情況,為了節省時間,簡單總結為以下幾點:
寬度測量使用:
int width=mPaint.measureText(mText);
mPaint.measureText(mText)精確度高於mBound.width()
高度測量使用:
FontMetrics fontMetrics = mPaint.getFontMetrics(); int height=Math.abs((fontMetrics.bottom - fontMetrics.top));
垂直居中方式:
FontMetricsInt fm = mPaint.getFontMetricsInt(); int startY = getHeight() / 2 - fm.descent + (fm.bottom - fm.top) / 2;
如果不明白的可以看上面那篇文章,很詳細說明。
其實一切沒多難,孰能生巧,現在寫這個自定控件的步驟都膩死了。還是簡單帶過
自定義View的屬性 在View的構造方法中獲得我們自定義的屬性 # 重寫onMesure # 重寫onDraw在 /value/attrs.xml 中,
text:文字,文字大小,文字默認顏色,文字高亮顏色
bitmap:默認圖片,高亮圖片
direction:圖片/文字變色的方向,左到右,右到左,上到下,下到上
load_style:加載方式,圖片或者文字
// 最大值 private static final float MAX = 100; // 系統默認:文字正常時顏色 private static final int TEXT_COLOR_NORMAL = Color.parseColor("#000000"); // 系統默認:文字高亮顏色 private static final int TEXT_COLOR_HIGHLIGHT = Color.parseColor("#FF0000"); // 繪制方向 private static final int LEFT = 1, TOP = 2, RIGHT = 3, BOTTOM = 4; // 文字樣式 private static final int STYLE_TEXT = 1; // 圖片樣式 private static final int STYLE_BITMAP = 2; // 順序繪制 private static final int LOAD_ASC = 0; // 反向/降序繪制 private static final int LOAD_DESC = 1; /** * 畫筆 */ private Paint mPaint; /** * 繪制的范圍 */ private Rect mBound; /** * 控件繪制位置起始的X,Y坐標值 */ private int mStartX = 0, mStartY = 0; /** * 文字大小 */ private int mTextSize = 16; /** * 文字正常顏色 */ private int mTextColorNormal = TEXT_COLOR_NORMAL; /** * 文字高亮顏色 */ private int mTextColorHighLight = TEXT_COLOR_HIGHLIGHT; /** * 文字 */ private String mText; /** * 繪制方向 */ private int mDirection = LEFT; /** * 控件風格 */ private int mLoadStyle = STYLE_TEXT; /** * bitmap正常/默認 */ private Bitmap mBitmapNormal; /** * bitmap高亮 */ private Bitmap mBitmapHighLight; /** * loading刻度 */ private float mProgress = 0; /** * 是否正在加載,避免開啟多個線程繪圖 */ private boolean mIsLoading = false; /** * 是否終止線程運行 */ private boolean mCanRun = true; /** * 加載方式{順序,反向} */ private int mLoadMode = LOAD_ASC; public RLoadView(Context context) { this(context, null); } public RLoadView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RLoadView); mText = a.getString(R.styleable.RLoadView_text); mTextColorNormal = a.getColor(R.styleable.RLoadView_text_color_normal, TEXT_COLOR_NORMAL); mTextColorHighLight = a.getColor( R.styleable.RLoadView_text_color_hightlight, TEXT_COLOR_HIGHLIGHT); mTextSize = a .getDimensionPixelSize(R.styleable.RLoadView_text_size, 16); mDirection = a.getInt(R.styleable.RLoadView_direction, LEFT); mLoadStyle = a.getInt(R.styleable.RLoadView_load_style, STYLE_TEXT); // 獲取bitmap mBitmapNormal = getBitmap(a, R.styleable.RLoadView_bitmap_src_normal); mBitmapHighLight = getBitmap(a, R.styleable.RLoadView_bitmap_src_hightlight); a.recycle(); /** * 初始化畫筆 */ mBound = new Rect(); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Style.FILL); if (mLoadStyle == STYLE_TEXT) { mPaint.setTextSize(mTextSize); mPaint.getTextBounds(mText, 0, mText.length(), mBound); } else if (mLoadStyle == STYLE_BITMAP) { mBound = new Rect(0, 0, mBitmapNormal.getWidth(), mBitmapNormal.getHeight()); } }
代碼很簡單,一眼帶過就可以
注意一下文字和圖片不同的繪制范圍控制(mBound)
在獲取圖片的時候要考慮到 點9 圖的情況,分兩種情況獲取
重寫onMesure 方法,重新測量控件的寬高
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = onMeasureR(0, widthMeasureSpec); int height = onMeasureR(1, heightMeasureSpec); setMeasuredDimension(width, height); }
/** * 計算控件寬高 * * @param attr屬性 * [0寬,1高] * @param oldMeasure * @author Ruffian */ public int onMeasureR(int attr, int oldMeasure) { int newSize = 0; int mode = MeasureSpec.getMode(oldMeasure); int oldSize = MeasureSpec.getSize(oldMeasure); switch (mode) { case MeasureSpec.EXACTLY: newSize = oldSize; break; case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: float value = 0; if (attr == 0) { if (mLoadStyle == STYLE_TEXT) { value = mPaint.measureText(mText); } else if (mLoadStyle == STYLE_BITMAP) { value = mBound.width(); } // newSize newSize = (int) (getPaddingLeft() + value + getPaddingRight()); } else if (attr == 1) { if (mLoadStyle == STYLE_TEXT) { FontMetrics fontMetrics = mPaint.getFontMetrics(); value = Math.abs((fontMetrics.bottom - fontMetrics.top)); } else if (mLoadStyle == STYLE_BITMAP) { value = mBound.height(); } // newSize newSize = (int) (getPaddingTop() + value + getPaddingBottom()); } break; } return newSize; }
文字和圖片的寬高獲取方式不同,需要判斷獲取。
可以先按照類型為文字的情況一路看下來思路比較清晰,理解了文字類型的繪制,再看圖片的更容易接受。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); /** * X,Y控件居中繪制 * 對於文本居中繪制 * 1.mPaint.measureText(mText)精確度高於mBound.width() * 2.文字高度測量:Math.abs((fontMetrics.bottom - fontMetrics.top)) * 3.http://blog.csdn.net/u014702653/article/details/51985821 */ if (mLoadStyle == STYLE_TEXT) { // 控件高度/2 + 文字高度/2,繪制文字從文字左下角開始,因此"+" FontMetricsInt fm = mPaint.getFontMetricsInt(); mStartY = getMeasuredHeight() / 2 - fm.descent + (fm.bottom - fm.top) / 2; mStartX = (int) (getMeasuredWidth() / 2 - mPaint.measureText(mText) / 2); } else if (mLoadStyle == STYLE_BITMAP) { mStartX = getMeasuredWidth() / 2 - mBound.width() / 2; mStartY = getMeasuredHeight() / 2 - mBound.height() / 2; } onDrawR(canvas); }
這段代碼說明一下 mStartX ,mStartY
mStartX:開始繪制文字/圖片的X軸起始坐標值,這裡要求水平居中
mStartY:開始繪制文字/圖片的Y軸起始坐標值,這裡要求垂直居中
特別注意:Android中文本會中Y軸是從文字底部開始繪制,
text:mStartY=控件高度/2 + 文字高度/2,繪制文字從文字左下角開始,因此”+”
/** * 繪制文字或者圖片 * * @param canvas * @param normalOrHightLight * [0:正常模式,1:高亮模式] * @param start * @param end * @author Ruffian */ protected void onDrawTextOrBitmap(Canvas canvas, int normalOrHightLight, int start, int end) { canvas.save(Canvas.CLIP_SAVE_FLAG); switch (mDirection) { case LEFT: case RIGHT: // X軸畫圖 canvas.clipRect(start, 0, end, getMeasuredHeight()); break; case TOP: case BOTTOM: // Y軸畫圖 canvas.clipRect(0, start, getMeasuredWidth(), end); break; } if (mLoadStyle == STYLE_TEXT) { // 繪制文字 if (normalOrHightLight == 0) { mPaint.setColor(mTextColorNormal); } else { mPaint.setColor(mTextColorHighLight); } canvas.drawText(mText, mStartX, mStartY, mPaint); } else if (mLoadStyle == STYLE_BITMAP) { // 繪制圖片 if (normalOrHightLight == 0) { canvas.drawBitmap(mBitmapNormal, mStartX, mStartY, mPaint); } else { canvas.drawBitmap(mBitmapHighLight, mStartX, mStartY, mPaint); } } canvas.restore(); }
繪制文字或者圖片的方法,保存當前畫布之後進行裁剪畫布在繪制文字。
比如說現在需要繪制“一串文字”的後半部分,就把前半部分裁減掉,然後繪制,當恢復畫布之後效果就是前面一半是空白,後面一半是文字
/** * 控件繪制 * * @param canvas * @author Ruffian */ public void onDrawR(Canvas canvas) { /** * 主要思想:繪制兩遍文字/圖像,通過裁剪畫布拼接兩部分文字/圖像,實現進度繪制的效果 */ // 需要變色的寬高總值(長度) int drawTotalWidth = 0; int drawTotalHeight = 0; // X,Y變色的進度實時值 int spliteXPro = 0; int spliteYPro = 0; // X,Y變色的最大值(坐標) int spliteXMax = 0; int spliteYMax = 0; // 開始變色的X,Y起始坐標值 int spliteYStart = 0; int spliteXStart = 0; FontMetricsInt fm = mPaint.getFontMetricsInt(); if (mLoadStyle == STYLE_TEXT) { drawTotalWidth = (int) mPaint.measureText(mText); drawTotalHeight = Math.abs(fm.ascent); spliteYStart = (fm.descent - fm.top) - Math.abs(fm.ascent) + getPaddingTop(); // 開始裁剪的Y坐標值:(http://img.blog.csdn.net/20160721172427552)圖中descent位置+paddingTop spliteYMax = Math.abs(fm.top) + (fm.descent); // Y變色(裁剪)的進度最大值(坐標):(http://img.blog.csdn.net/20160721172427552)看圖 } else if (mLoadStyle == STYLE_BITMAP) { drawTotalWidth = mBound.width(); drawTotalHeight = mBound.height(); spliteYStart = mStartY;// 開始裁剪的Y坐標值:圖片開始繪制的地方 spliteYMax = mStartY + drawTotalHeight; // Y變色(裁剪)的進度最大值(坐標):圖片開始繪制的地方+需要變色(裁剪)的高總值(長度) } spliteXPro = (int) ((mProgress / MAX) * drawTotalWidth); spliteYPro = (int) ((mProgress / MAX) * drawTotalHeight); spliteXStart = mStartX;// 開始裁剪的X坐標值:文字開始繪畫的地方 spliteXMax = mStartX + drawTotalWidth; // X變色(裁剪)的進度最大值(坐標):X變色(裁剪)起始位置+需要變色(裁剪)的寬總值(長度) switch (mDirection) { case TOP: // 從上到下,分界線上邊是高亮顏色,下邊是原始默認顏色 onDrawTextOrBitmap(canvas, 1, spliteYStart, spliteYStart + spliteYPro); onDrawTextOrBitmap(canvas, 0, spliteYStart + spliteYPro, spliteYMax); break; case BOTTOM: // 從下到上,分界線下邊是默認顏色 ,上邊是高亮顏色 onDrawTextOrBitmap(canvas, 0, spliteYStart, spliteYMax - spliteYPro); onDrawTextOrBitmap(canvas, 1, spliteYMax - spliteYPro, spliteYMax); break; case LEFT: // 從左到右,分界線左邊是高亮顏色,右邊是原始默認顏色 onDrawTextOrBitmap(canvas, 1, spliteXStart, spliteXStart + spliteXPro); onDrawTextOrBitmap(canvas, 0, spliteXStart + spliteXPro, spliteXMax); break; case RIGHT: // 從右到左,分界線左邊是默認顏色 ,右邊是高亮顏色 onDrawTextOrBitmap(canvas, 0, spliteXStart, spliteXMax - spliteXPro); onDrawTextOrBitmap(canvas, 1, spliteXMax - spliteXPro, spliteXMax); break; } }
這個方法是控制裁剪起始和終止坐標值的關鍵方法。
可以結合這篇博客理解
說一下思路,思路很關鍵啊,拿筆記好啦。哈哈
文章開頭就說了,文字/圖片變色原理是使用兩種不同的顏色(或者兩張圖片)繪制兩遍文字/圖片
先看幾個變量
// 需要變色的寬高總值(長度) int drawTotalWidth = 0; int drawTotalHeight = 0; // X,Y變色的進度實時值 int spliteXPro = 0; int spliteYPro = 0; // X,Y變色的最大值(坐標) int spliteXMax = 0; int spliteYMax = 0; // 開始變色的X,Y起始坐標值 int spliteYStart = 0; int spliteXStart = 0;需要變色的寬度總值(drawTotalWidth ):這裡就是文字或者圖片自身的寬度 X變色的進度實時值(spliteXPro ):表示變色進度的實時值。
好好理解這幾個概念,接下來就是繪制控件了。
繪制的原理都是一樣的,只要理解了,從哪個方向開始變色都是一樣的,無非是起始值和結束值的計算。計算之前一定要搞清楚上面的幾個概念,不然,呵呵。說句不炫耀的話,寫代碼的時候,我差點把自己弄暈。
// 從左到右,分界線左邊是高亮顏色,右邊是原始默認顏色 onDrawTextOrBitmap(canvas, 1, spliteXStart, spliteXStart + spliteXPro); onDrawTextOrBitmap(canvas, 0, spliteXStart + spliteXPro, spliteXMax);
onDrawTextOrBitmap(canvas, 1, spliteXStart, spliteXStart + spliteXPro);
第一個參數表示畫布,
第二個參數表示文本類型(1:高亮,0:默認),
第三個參數變色開始的X值,
第四個參數變色結束X值
先繪制一遍高亮顏色的文字,再繪制一遍默認顏色的。
開始變色 ↓- - - - - - - - - - 變色最大值
高亮顏色從 開始變色的地方,繪制到,某個值停止
默認顏色從 高亮顏色停止的位置,繪制到,變色最大值位置
從progress=0,到progress=100,隨著progress增加 高亮顏色慢慢向右遞增,默認顏色慢慢遞減,形成變色效果
其實就是畫布裁剪出一部分來繪制 高亮顏色,然後再裁剪出一部分顏色來繪制 默認顏色。
如果進度停在50%,就會看到一半高亮,一半默認
OK,看看這個自定義類裡面都有哪些方法
RLoadView:構造方法,獲取基本屬性 #onMeasure:重寫onMeasure方法計算控件寬高 onMeasureR():自定義方法,計算控件寬高 #onDraw:重寫onDraw方法,繪制控件 onDrawR():自定義方法,繪制控件之前計算,處理 onDrawTextOrBitmap:自定義方法,繪制文本/圖片邏輯 start:自定義方法,開始執行文本/圖片變色 stop:自定義方法,結束文本/圖片變色 getProgress:獲取進度值 setProgress:設置進度值 getBitmap:獲取圖片屬性,區分 點9圖 #onSaveInstanceState:重寫方法,保存信息(進度值等) #onRestoreInstanceState:重寫方法,重新設置信息(進度值等)其中對外提供的方法:start,stop,getProgress,setProgress
public void start(final long duration, final boolean isRepeat, final boolean isReverse)
start 開始執行loading,使用在 LoadingView 情形中
第一參數:執行時間,設置變色執行的時間
第二個參數:是否循環重新變色,true循環變色,並且重頭開始變色
第三個參數:是否反向褪色,true則表示變色完成之後反向褪色
可以自由組合第二第三個參數,實現不同的效果。
public void stop()
停止變色。配合 start 使用,由於start開啟子線程實現變色,通過stop停止線程執行,停止變色。
public void setProgress(float progress)
設置進度值。使用在ProgressBar 情形中。
在下載文件情形下,實時設置progress,方法會重繪控件,更新進度條。
public float getProgress()
獲取進度值。更適合使用在 ProgressBar 情形中。
在 LoadingView 情形中獲取進度值沒有意義,會在0-100之間不斷變化。
//獲取自定義控件 RLoadView mLoadView = (RLoadView) findViewById(R.id.id_loadView); /** * 使用情形1:LoadingView */ //變色時間,變色模式,開始LoadingView mLoadView.start(1500, true, false); //停止Loading mLoadView.stop(); /** * 使用情形2:ProgressBar */ //設置進度值,模擬下載設置下載進度值 mLoadView.setProgress(mProgress); //獲取進度值,模擬下載獲取已下載進度值 mLoadView.getProgress();
使用起來從未如此簡單,方便。兩行代碼搞定一切的既視感!關鍵是效果酷炫!
還在等什麼?趕快下載體驗吧
Github:https://github.com/RuffianZhong/RLoadView
順便說一下另外一個自定義控件,簡單實用的ViewPageIndicator,RVPIndicator
高仿MIUI但更勝於MIUI,提供多種指示器類型{下滑線,三角形,全背景}
覺得這不滿足你的需求?沒問題,RVPIndicator 還支持使用圖片作為指示器。一張圖實現你的願望
不會作圖?你想自定義?OK,添加兩三行代碼就可以增加新的指示器樣式
rvp:indicator_color="#f29b76" //指示器顏色 rvp:indicator_src="@drawable/heart_love" //指示器圖片{指示器類型為bitmap時需要} rvp:indicator_style="triangle" //指示器類型 //{bitmap:圖片;line:下劃線;square:方形全背景;triangle:三角形} rvp:item_count="4" //item展示個數 rvp:text_color_hightlight="#FF0000" //item文字高亮顏色 rvp:text_color_normal="#fb9090" //item文字正常顏色
// 設置Tab上的標題 mIndicator.setTabItemTitles(mDatas); // 設置關聯的ViewPager mIndicator.setViewPager(mViewPager, 0);
Github:https://github.com/RuffianZhong/RVPIndicator
敢不敢留個言,點個贊,證明真的有人在看,哈哈
本文實例講述了Android編程使用android-support-design實現MD風格對話框功能。分享給大家供大家參考,具體如下:首先上效果圖: 測試設備
ProgressBar作用:當應在後台執行時,前台界面不會有任何信息,這時用戶根本不知道程序是否在執行,以及執行進度等,因些需要使用進度條來提示程序執行的進度.在Andr
之前寫過一篇關於Android 繼承DialogFragment彈出dialog對話框一,這次是在上次的基礎上修改了一些東西,就是怎樣在DialogFragment中獲取
好久沒寫android的博客,最近在做一個android的項目,裡面用到我們經常用的一個控件就是對話框,大家都知道android自帶的對話框是很丑的,android5.x
Agenda:一張圖看Camera2框架類圖 CameraService