前言
老早以前就開(kāi)始看關(guān)于自定義View的文章了畔柔,這兩天實(shí)踐了一下以前看的有關(guān)自定義View的知識(shí)氯夷,畫(huà)了個(gè)控件。這個(gè)控件雖然看起來(lái)簡(jiǎn)單靶擦,但自定義View的基本用法都用到了腮考。麻雀雖小,五臟俱全玄捕,所以還是記錄一下過(guò)程踩蔚。
關(guān)于自定義View的文章,我在這墻裂推薦一下GcsSloop寫(xiě)的安卓自定義View教程枚粘。這是目前我認(rèn)為寫(xiě)得比較好的關(guān)于自定義View的文章馅闽,而且很完整也很詳細(xì),通俗易懂馍迄。
那么福也,回歸主題,就從畫(huà)個(gè)不簡(jiǎn)單的圓開(kāi)始吧攀圈。先看效果圖暴凑,我知道沒(méi)有圖你們是不會(huì)往下看的。
gif圖的效果有點(diǎn)卡頓赘来,真機(jī)上是很流暢的现喳。
實(shí)現(xiàn)過(guò)程
拿到一個(gè)效果圖,先看看要怎么畫(huà)犬辰,思考清楚再去干活有時(shí)候會(huì)事半功倍(其實(shí)剛開(kāi)始我只是打算畫(huà)個(gè)圓嗦篱,畫(huà)著畫(huà)著就越來(lái)越多了...)。我們這效果很簡(jiǎn)單忧风,我們現(xiàn)在的需求只是要畫(huà)個(gè)圓默色,然后再在上面寫(xiě)個(gè)字。
畫(huà)個(gè)圓
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
float centerX = paddingLeft + width / 2;
float centerY = paddingTop + height / 2;
float radius = Math.min(width, height) / 2;
canvas.drawCircle(centerX, centerY, radius, mCirclePaint);
}
畫(huà)圓的代碼很簡(jiǎn)單狮腿,因?yàn)槭抢^承自View腿宰,所以需要先在onDrow方法中先進(jìn)行處理padding,不然padding會(huì)失效缘厢。拿到我們的View的實(shí)際的寬高后吃度,再去獲取View的中心點(diǎn),用來(lái)當(dāng)作圓心位置贴硫。因?yàn)槲疫@里的畫(huà)布的坐標(biāo)原點(diǎn)在屏幕左上角椿每,所以獲取中心點(diǎn)的時(shí)候要加上paddingLeft和paddingTop的距離伊者。centerX和centerY組成的點(diǎn)是我們布局的中心點(diǎn)。拿到中心點(diǎn)后间护,再根據(jù)長(zhǎng)寬中的最小值的一半當(dāng)作半徑亦渗,然后就可以進(jìn)行繪制圓了。So easy!
寫(xiě)個(gè)字
drawText我們用到的是四個(gè)參數(shù)的方法汁尺,這里有個(gè)要注意的地方就是第二個(gè)參數(shù)和第三個(gè)參數(shù)并不是文字的中心位置法精,分別是文字左邊的X值和文字底部的Y值〕胀唬看下源碼就知道了:
/**
* 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);
}
那么這個(gè)baseline又是什么搂蜓?baseline就是文字底部的Y值×勺埃看看下面這張圖你就明白了:
想要把文字放在圓的中心位置帮碰,先要找到baseline和文字的左邊X值。
private void drawVerticalText(Canvas canvas, float centerX, float centerY, String text) {
Paint.FontMetrics fontMetrics = mVerticalTextPaint.getFontMetrics();
float baseLine = -(fontMetrics.ascent + fontMetrics.descent) / 2;
float textWidth = mVerticalTextPaint.measureText(text);
float startX = centerX - textWidth / 2;
float endY = centerY + baseLine;
canvas.drawText(text, startX, endY, mVerticalTextPaint);
}
網(wǎng)上有很多關(guān)于drawText的文章拾积,不過(guò)多數(shù)是用一個(gè)Rect包裹著再畫(huà)殉挽,我這里是直接畫(huà)。畫(huà)字在這里我封裝成了一個(gè)方法拓巧,從畫(huà)筆中拿到FontMetrics對(duì)象此再,用來(lái)獲取文字的屬性,包括Ascent值和Descent值等玲销。然后計(jì)算baseline,用Paint的measureText方法獲取文字的長(zhǎng)度摘符。最后根據(jù)中心點(diǎn)偏移一下把文字放到中間贤斜,ok」淇悖看一下效果:
文字豎排
這時(shí)瘩绒,又有個(gè)需求,我想要文字豎排顯示怎么弄带族?第一反應(yīng)想到的是drawText是不是應(yīng)該有個(gè)可以控制方向的方法或參數(shù)锁荔?然而,并沒(méi)有這么容易蝙砌。drawText并不能豎排畫(huà)文字阳堕,那么drawTextOnPath方法是不是可以呢?在圓的豎直方向建一條路徑择克,但是結(jié)果并不是很理想恬总,圖我就不放了。最后想到的是用StaticLayout這個(gè)類(lèi)肚邢,通過(guò)換行來(lái)實(shí)現(xiàn)豎排壹堰,其實(shí)TextView源碼中也用到了這個(gè)來(lái)?yè)Q行拭卿。來(lái)看主要代碼:
...
mHorizontalTextPaint = new TextPaint(mVerticalTextPaint);
mStaticLayout = new StaticLayout(mText, mHorizontalTextPaint, 1, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false);
...
private void drawHorizontalText(Canvas canvas, float centerX, float centerY) {
canvas.translate(centerX, centerY - mStaticLayout.getHeight() / 2);
mStaticLayout.draw(canvas);
}
StaticLayout的構(gòu)造方法第二個(gè)參數(shù)需要傳個(gè)TextPaint進(jìn)去,TextPaint是繼承自Paint贱纠,所以可以直接傳個(gè)我們上面畫(huà)水平方向文字的時(shí)候的Paint對(duì)象進(jìn)去就行了峻厚,就不用重新設(shè)置顏色字體大小等屬性。關(guān)于StaticLayout的構(gòu)造方法的參數(shù)是什么作用我這就不詳細(xì)說(shuō)了谆焊,我們主要看第三個(gè)參數(shù)惠桃。這個(gè)參數(shù)代表超過(guò)多少個(gè)字符就換行,這里我們傳個(gè)1代表一個(gè)就換行懊渡。然后把畫(huà)布X軸移動(dòng)到圓心坐標(biāo)刽射,文字的高度的一半為圓心Y軸的值。這樣剃执,我們就實(shí)現(xiàn)了文字豎排:
文字任意角度
繼續(xù)加需求誓禁,要求文字45°斜著顯示該怎么弄?這個(gè)簡(jiǎn)單肾档,把畫(huà)布坐標(biāo)旋轉(zhuǎn)不就可以了嗎~
canvas.rotate(mAngle, centerX, centerY);
drawText(canvas, centerX, centerY);
調(diào)用畫(huà)布的rotate方法就可以了摹恰,就一行代碼。但是這里要注意的是在drawText前面旋轉(zhuǎn)畫(huà)布后再drawText怒见,一定要在drawText前面旋轉(zhuǎn)畫(huà)布俗慈!不然是沒(méi)效果的持寄,我在這就跳進(jìn)坑里了T.T 在drawText后才旋轉(zhuǎn)畫(huà)布腋颠,一直沒(méi)反應(yīng)。還以為是哪里出錯(cuò)了州胳。
旋轉(zhuǎn)
靜態(tài)的文字頭像有點(diǎn)單調(diào)了舵变,那就加個(gè)動(dòng)畫(huà)吧酣溃。好,說(shuō)加就加纪隙,加個(gè)旋轉(zhuǎn)的吧赊豌。
public void startRotateAnimation() {
if (mAnimator == null) {
initRotateAnimation();
}
if (!mAnimator.isRunning()) {
mAnimator.start();
}
}
private void initRotateAnimation() {
if (mRotateOrientation == ROTATE_CLOCKWISE) {
mRotateValues = 360f;
} else if (mRotateOrientation == ROTATE_ANTICLOCKWISE) {
mRotateValues = -360f;
}
mAnimator = ValueAnimator.ofFloat(0f, mRotateValues);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.setDuration(mSpeed);
mAnimator.addUpdateListener(this);
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
mAnimator.setRepeatMode(ValueAnimator.RESTART);
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAngle = (float) animation.getAnimatedValue();
invalidate();
}
public void stopRotateAnimation() {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
}
}
關(guān)于旋轉(zhuǎn)動(dòng)畫(huà)的主要就這四個(gè)方法。原理是用屬性動(dòng)畫(huà)ValueAnimator動(dòng)態(tài)線性均勻改變一個(gè)0到360浮點(diǎn)值绵咱,在AnimatorUpdateListener的onAnimationUpdate方法獲取范圍值碘饼,mAngle為上面任意角度文字的值,動(dòng)態(tài)改變這個(gè)值悲伶,不斷重繪界面艾恼,就可以實(shí)現(xiàn)文字的旋轉(zhuǎn)動(dòng)畫(huà)了。至于上面的360f和-360f是用來(lái)控制方向的拢切,順時(shí)針或者逆時(shí)針旋轉(zhuǎn)蒂萎。通過(guò)持續(xù)時(shí)間setDuration來(lái)控制速度。轉(zhuǎn)起來(lái):
縮放
看到FloatingActionButton自帶的show和hide的動(dòng)畫(huà)淮椰,這里我也仿照著弄了個(gè)五慈。
public void show() {
if (!isShow) {
scaleAnimation(0.0f, 1.0f, 300);
isShow = true;
}
}
public void hide() {
if (isShow) {
scaleAnimation(1.0f, 0.0f, 300);
isShow = false;
}
}
private void scaleAnimation(float start, float end, long duration) {
ObjectAnimator animatorX = ObjectAnimator.ofFloat(this, "scaleX", start, end);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(this, "scaleY", start, end);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(duration);
animatorSet.playTogether(animatorX, animatorY);
animatorSet.start();
}
同樣是通過(guò)屬性動(dòng)畫(huà)ObjectAnimator纳寂,去縮放X軸方向“scaleX”,和Y軸方向“scaleY”泻拦。把兩個(gè)動(dòng)畫(huà)放到一個(gè)動(dòng)畫(huà)集AnimatorSet里面同時(shí)執(zhí)行毙芜,從0f到1f是顯示,1f到0f是隱藏争拐。
結(jié)語(yǔ)
至此腋粥,這個(gè)文字頭像控就控件就寫(xiě)完了。其實(shí)沒(méi)什么很難的地方架曹,只是把自定義View的基本流程寫(xiě)了一遍隘冲。對(duì)于學(xué)習(xí)自定義View,光看是不行的绑雄,一定要實(shí)踐展辞,動(dòng)手后發(fā)現(xiàn)也沒(méi)想象中的那么難。
同時(shí)万牺,我將這個(gè)CircleView封裝了一下發(fā)布到了JCenter罗珍,免得重復(fù)造輪子。
項(xiàng)目地址:https://github.com/finalrose7/CircleView
喜歡就點(diǎn)個(gè)Star吧脚粟!