今天朋友看了HenCoder的自定義View後說,HenCoder對自定義View講的不錯。實踐中仿寫即刻的點贊你有思路嗎,你不實現一下?二話不說,看了朋友手機效果,對他說:實現不難,用到了位移,縮放,漸變更畫和自定義View的基礎用法,好,那我實現一下,恰好加深對自定義View的理解。php
把即刻app下載後,以解壓包的方式解壓,發現點贊效果有三張圖,一張是沒有點讚的小手圖片,一張是點贊後的紅色小手圖片,最後一張是點贊後,點贊手指上的四點以下圖: java
在values下的attrs文件下添加屬性集合以下:git
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--name爲聲明的屬性集合,能夠隨意取,最好是和自定義View同樣的名稱,這樣方便管理-->
<declare-styleable name="JiKeLikeView">
<!-- 聲明屬性,名稱爲like_number,取值是整形-->
<attr name="like_number" format="integer"/>
</declare-styleable>
</resources>
複製代碼
由於點贊只涉及到數字,因此聲明和定義整形便可。 新建一個類繼承View,並在構造函數中,讀取attrs文件下配置屬性:github
public JiKeLikeView(Context context) {
this(context, null);
}
public JiKeLikeView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public JiKeLikeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//獲取attrs文件下配置屬性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.JiKeLikeView);
//點贊數量 第一個參數就是屬性集合裏面的屬性 固定格式R.styleable+自定義屬性名字
//第二個參數,若是沒有設置這個屬性,則會取設置的默認值
likeNumber = typedArray.getInt(R.styleable.JiKeLikeView_like_number, 1999);
//記得把TypedArray對象回收
typedArray.recycle();
init();
}
複製代碼
init方法是初始化一些畫筆,文本顯示範圍canvas
private void init() {
//建立文本顯示範圍
textRounds = new Rect();
//點贊數暫時8位
widths = new float[8];
//Paint.ANTI_ALIAS_FLAG 屬性是位圖抗鋸齒
//bitmapPaint是圖像畫筆
bitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//這是繪製原來數字的畫筆 加入沒點贊以前是45 那麼點贊後就是46 點贊是46 那麼沒點贊就是45
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
oldTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//文字顏色大小配置 顏色灰色 字體大小爲14
textPaint.setColor(Color.GRAY);
textPaint.setTextSize(SystemUtil.sp2px(getContext(), 14));
oldTextPaint.setColor(Color.GRAY);
oldTextPaint.setTextSize(SystemUtil.sp2px(getContext(), 14));
//圓畫筆初始化 Paint.Style.STROKE只繪製圖形輪廓
circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
circlePaint.setColor(Color.RED);
circlePaint.setStyle(Paint.Style.STROKE);
//設置輪廓寬度
circlePaint.setStrokeWidth(SystemUtil.dp2px(getContext(), 2));
//設置模糊效果 第一個參數是模糊半徑,越大越模糊,第二個參數是陰影的橫向偏移距離,正值向下偏移 負值向上偏移
//第三個參數是縱向偏移距離,正值向下偏移,負值向上偏移 第四個參數是畫筆的顏色
circlePaint.setShadowLayer(SystemUtil.dp2px(getContext(), 1), SystemUtil.dp2px(getContext(), 1), SystemUtil.dp2px(getContext(), 1), Color.RED);
}
複製代碼
在onAttachedToWindow方法上建立Bitmap對象數組
/** * 這個方法是在Activity resume的時候被調用的,Activity對應的window被添加的時候 * 每一個view只會調用一次,能夠作一些初始化操做 */
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Resources resources = getResources();
//構造Bitmap對象,經過BitmapFactory工廠類的static Bitmap decodeResource根據給定的資源id解析成位圖
unLikeBitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_message_unlike);
likeBitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_message_like);
shiningBitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_message_like_shining);
}
複製代碼
至於爲何要在這個方法構建而不寫在init方法,上面代碼附帶了解釋。 另外要在onDetachedFromWindow方法回收bitmap微信
/** * 和onAttachedToWindow對應,在destroy view的時候調用 */
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//回收bitmap
unLikeBitmap.recycle();
likeBitmap.recycle();
shiningBitmap.recycle();
}
複製代碼
構造了三個Bitmap對象,上面分析很清楚了,一個是小手上的四點,一個是點贊小手,最後一個是沒點讚的小手。app
/** * 測量寬高 * 這兩個參數是由父視圖通過計算後傳遞給子視圖 * @param widthMeasureSpec 寬度 * @param heightMeasureSpec 高度 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//MeasureSpec值由specMode和specSize共同組成,onMeasure兩個參數的做用根據specMode的不一樣,有所區別。
//當specMode爲EXACTLY時,子視圖的大小會根據specSize的大小來設置,對於佈局參數中的match_parent或者精確大小值
//當specMode爲AT_MOST時,這兩個參數只表示了子視圖當前可使用的最大空間大小,而子視圖的實際大小不必定是specSize。因此咱們自定義View時,重寫onMeasure方法主要是在AT_MOST模式時,爲子視圖設置一個默認的大小,對於佈局參數wrap_content。
//高度默認是bitmap的高度加上下margin各10dp
heightMeasureSpec = MeasureSpec.makeMeasureSpec(unLikeBitmap.getHeight() + SystemUtil.dp2px(getContext(), 20), MeasureSpec.EXACTLY);
//寬度默認是bitmap的寬度加左右margin各10dp和文字寬度和文字右側10dp likeNumber是文本數字
String textnum = String.valueOf(likeNumber);
//獲得文本的寬度
float textWidth = textPaint.measureText(textnum, 0, textnum.length());
//計算整個View的寬度 小手寬度 + 文本寬度 + 30px
widthMeasureSpec = MeasureSpec.makeMeasureSpec(((int) (unLikeBitmap.getWidth() + textWidth + SystemUtil.dp2px(getContext(), 30))), MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
複製代碼
至於上面爲何用MeasureSpec.EXACTLY,上面已經解釋很清楚了。ide
super.onDraw(canvas);
//獲取正個View的高度
int height = getHeight();
//取中心
int centerY = height / 2;
//小手根據有沒有點贊進行改變
Bitmap handBitmap = isLike ? likeBitmap : unLikeBitmap;
//獲得圖像寬度
int handBitmapWidth = handBitmap.getWidth();
//獲得圖像高度
int handBitmapHeight = handBitmap.getHeight();
//畫小手
int handTop = (height - handBitmapHeight) / 2;
//先保存畫布的狀態
canvas.save();
//根據bitmap中心進行縮放
canvas.scale(handScale, handScale, handBitmapWidth / 2, centerY);
//畫bitmap小手,第一個是參數對應的bitmap,第二個參數是左上角座標,第三個參數上頂部座標,第四個是畫筆
canvas.drawBitmap(handBitmap, SystemUtil.dp2px(getContext(), 10), handTop, bitmapPaint);
//讀取以前沒有縮放畫布的狀態
canvas.restore();
複製代碼
這裏解釋一下爲何用到canvas.save()和canvas.restore()呢,由於整個點贊效果是有動畫效果的,對畫布進行縮放,若是不保存畫布以前的狀態,縮放後繼續繪製其餘圖像效果並非你想要的。函數
//畫上面四點閃亮
//先肯定頂部
int shiningTop = handTop - shiningBitmap.getHeight() + SystemUtil.dp2px(getContext(), 17);
//根據隱藏係數設置點亮的透明度
bitmapPaint.setAlpha((int) (255 * shiningAlpha));
//保存畫布狀態
canvas.save();
//畫布根據點亮的縮放係數進行縮放
canvas.scale(shiningScale, shiningScale, handBitmapWidth / 2, handTop);
//畫出點亮的bitmap
canvas.drawBitmap(shiningBitmap, SystemUtil.dp2px(getContext(), 15), shiningTop, bitmapPaint);
//恢復畫筆以前的狀態
canvas.restore();
//而且恢復畫筆bitmapPaint透明度
bitmapPaint.setAlpha(255);
複製代碼
注意只是用了bitmapPaint.setAlpha()方法設置這四點是否顯示和消失,設置上這四點都是存在畫布上的,點贊後設置setAlpha(255)出現,不然根據透明度來進行顯示,有個變化的趨勢。
這裏分兩種大狀況,一種是不一樣位數的數字變化,另一種是同位數數字變化
//畫文字
String textValue = String.valueOf(likeNumber);
//若是點讚了,以前的數值就是點贊數-1,若是取消點贊,那麼以前數值(對比點贊後)就是如今顯示的
String textCancelValue;
if (isLike) {
textCancelValue = String.valueOf(likeNumber - 1);
} else {
if (isFirst) {
textCancelValue = String.valueOf(likeNumber + 1);
} else {
isFirst = !isFirst;
textCancelValue = String.valueOf(likeNumber);
}
}
//文本的長度
int textLength = textValue.length();
//獲取繪製文字的座標 getTextBounds 返回全部文本的聯合邊界
textPaint.getTextBounds(textValue, 0, textValue.length(), textRounds);
//肯定X座標 距離手差10dp
int textX = handBitmapWidth + SystemUtil.dp2px(getContext(), 20);
//肯定Y座標 距離 大圖像的一半減去 文字區域高度的一半 便可得出 getTextBounds裏的rect參數獲得數值後,
// 查看它的屬性值 top、bottom會發現top是一個負數;bottom有時候是0,有時候是正數。結合第一點很容易理解,由於baseline座標當作原點(0,0),
// 那麼相對位置top在它上面就是負數,bottom跟它重合就爲0,在它下面就爲負數。像小寫字母j g y等,它們的bounds bottom都是正數,
// 由於它們都有降部(在西文字體排印學中,降部指的是一個字體中,字母向下延伸超過基線的筆畫部分)。
int textY = height / 2 - (textRounds.top + textRounds.bottom) / 2;
//繪製文字 這種狀況針對不一樣位數變化 如 99 到100 999到10000
if (textLength != textCancelValue.length() || textMaxMove == 0) {
//第一個參數就是文字內容,第二個參數是文字的X座標,第三個參數是文字的Y座標,注意這個座標
//並非文字的左上角 而是與左下角比較接近的位置
//canvas.drawText(textValue, textX, textY, textPaint);
//點贊
if (isLike) {
//圓的畫筆根據設置的透明度進行變化
circlePaint.setAlpha((int) (255 * shingCircleAlpha));
//畫圓
canvas.drawCircle(handBitmapWidth / 2 + SystemUtil.dp2px(getContext(), 10), handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 10), ((handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 2)) * shingCircleScale), circlePaint);
//根據透明度進行變化
oldTextPaint.setAlpha((int) (255 * (1 - textAlpha)));
//繪製以前的數字
canvas.drawText(textCancelValue, textX, textY - textMaxMove + textMoveDistance, oldTextPaint);
//設置新數字的透明度
textPaint.setAlpha((int) (255 * textAlpha));
//繪製新數字(點贊後或者取消點贊)
canvas.drawText(textValue, textX, textY + textMoveDistance, textPaint);
} else {
oldTextPaint.setAlpha((int) (255 * (1 - textAlpha)));
canvas.drawText(textCancelValue, textX, textY + textMaxMove + textMoveDistance, oldTextPaint);
textPaint.setAlpha((int) (255 * textAlpha));
canvas.drawText(textValue, textX, textY + textMoveDistance, textPaint);
}
return;
}
//下面這種狀況區別與99 999 9999這種 就是相同位數變化
//把文字拆解成一個一個字符 就是獲取字符串中每一個字符的寬度,把結果填入參數widths
//至關於measureText()的一個快捷方法,計算等價於對字符串中的每一個字符分別調用measureText(),並把
//它們的計算結果分別填入widths的不一樣元素
textPaint.getTextWidths(textValue, widths);
//將字符串轉換爲字符數組
char[] chars = textValue.toCharArray();
char[] oldChars = textCancelValue.toCharArray();
for (int i = 0; i < chars.length; i++) {
if (chars[i] == oldChars[i]) {
textPaint.setAlpha(255);
canvas.drawText(String.valueOf(chars[i]), textX, textY, textPaint);
} else {
//點贊
if (isLike) {
circlePaint.setAlpha((int) (255 * shingCircleAlpha));
canvas.drawCircle(handBitmapWidth / 2 + SystemUtil.dp2px(getContext(), 10), handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 10), ((handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 2)) * shingCircleScale), circlePaint);
oldTextPaint.setAlpha((int) (255 * (1 - textAlpha)));
canvas.drawText(String.valueOf(oldChars[i]), textX, textY - textMaxMove + textMoveDistance, oldTextPaint);
textPaint.setAlpha((int) (255 * textAlpha));
canvas.drawText(String.valueOf(chars[i]), textX, textY + textMoveDistance, textPaint);
} else {
oldTextPaint.setAlpha((int) (255 * (1 - textAlpha)));
canvas.drawText(String.valueOf(oldChars[i]), textX, textY + textMaxMove + textMoveDistance, oldTextPaint);
textPaint.setAlpha((int) (255 * textAlpha));
canvas.drawText(String.valueOf(chars[i]), textX, textY + textMoveDistance, textPaint);
}
}
//下一位數字x座標要加上前一位的寬度
textX += widths[i];
}
複製代碼
我這裏用了textValue和textCancelValue分別記錄變化先後的數字,下面可能對肯定y座標的代碼有疑問,這裏解釋一下:
int textY = height / 2 - (textRounds.top + textRounds.bottom) / 2;
複製代碼
這裏textRounds.top是負數,座標原點並非在左上角,而是在文本的基線中,本身再查查相關資料和想一想就明白了,上面代碼也有解釋。 透明度變化就不詳細講了,這裏講講移動距離:
//點贊
if (isLike) {
//圓的畫筆根據設置的透明度進行變化
circlePaint.setAlpha((int) (255 * shingCircleAlpha));
//畫圓
canvas.drawCircle(handBitmapWidth / 2 + SystemUtil.dp2px(getContext(), 10), handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 10), ((handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 2)) * shingCircleScale), circlePaint);
//根據透明度進行變化
oldTextPaint.setAlpha((int) (255 * (1 - textAlpha)));
//繪製以前的數字
canvas.drawText(textCancelValue, textX, textY - textMaxMove + textMoveDistance, oldTextPaint);
//設置新數字的透明度
textPaint.setAlpha((int) (255 * textAlpha));
//繪製新數字(點贊後或者取消點贊)
canvas.drawText(textValue, textX, textY + textMoveDistance, textPaint);
} else {
oldTextPaint.setAlpha((int) (255 * (1 - textAlpha)));
canvas.drawText(textCancelValue, textX, textY + textMaxMove + textMoveDistance, oldTextPaint);
textPaint.setAlpha((int) (255 * textAlpha));
canvas.drawText(textValue, textX, textY + textMoveDistance, textPaint);
}
複製代碼
textMaxMove設置是20px,textMoveDistance設置是文字的高度14px
//繪製以前的數字
canvas.drawText(textCancelValue, textX, textY - textMaxMove + textMoveDistance, oldTextPaint);
//繪製新數字(點贊後或者取消點贊)
canvas.drawText(textValue, textX, textY + textMoveDistance, textPaint);
複製代碼
這兩行就是繪製新數字,最主要就是y座標的變化,舉個例子應該很好理解:假如如今104,我如今點贊要變成105,textCancelValue是104,textValue是105.由於textMoveDistance是從20變化0逐漸減小的,那麼第一條公式是繪製105,textY - textMaxMove + textMoveDistance,y座標愈來愈小,因此5就會上移,同理textY + textMoveDistance 根據這條公式4也會上移,由於數值愈來愈小,還有就是將數字轉換爲字符串進行處理不難理解。 畫圓圈擴散主要是肯定圓圈中心點,半徑大概肯定就行:
canvas.drawCircle(handBitmapWidth / 2 + SystemUtil.dp2px(getContext(), 10), handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 10), ((handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 2)) * shingCircleScale), circlePaint);
複製代碼
前兩個參數就是肯定圓中心,我設置在小手圖像中心。
我是設置觸摸就觸發點贊事件:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
jump();
break;
}
return super.onTouchEvent(event);
}
複製代碼
jump方法以下:
/** * 點贊事件觸發 */
private void jump() {
isLike = !isLike;
if (isLike) {
++likeNumber;
setLikeNum();
//自定義屬性 在ObjectAnimator中,是先根據屬性值拼裝成對應的set函數名字,好比下面handScale的拼裝方法就是
//將屬性的第一個字母強制大寫後與set拼接,因此就是setHandScale,而後經過反射找到對應控件的setHandScale(float handScale)函數
//將當前數字值作爲setHandScale(float handScale)的參數傳入 set函數調用每隔十幾毫秒就會被用一次
//ObjectAnimator只負責把當前運動動畫的數值傳給set函數,set函數怎麼來作就在裏面寫就行
ObjectAnimator handScaleAnim = ObjectAnimator.ofFloat(this, "handScale", 1f, 0.8f, 1f);
//設置動畫時間
handScaleAnim.setDuration(duration);
//動畫 點亮手指的四點 從0 - 1出現
ObjectAnimator shingAlphaAnim = ObjectAnimator.ofFloat(this, "shingAlpha", 0f, 1f);
// shingAlphaAnim.setDuration(duration);
//放大 點亮手指的四點
ObjectAnimator shingScaleAnim = ObjectAnimator.ofFloat(this, "shingScale", 0f, 1f);
//畫中心圓形有內到外擴散
ObjectAnimator shingClicleAnim = ObjectAnimator.ofFloat(this, "shingCircleScale", 0.6f, 1f);
//畫出圓形有1到0消失
ObjectAnimator shingCircleAlphaAnim = ObjectAnimator.ofFloat(this, "shingCircleAlpha", 0.3f, 0f);
//動畫集一塊兒播放
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(handScaleAnim, shingAlphaAnim, shingScaleAnim, shingClicleAnim, shingCircleAlphaAnim);
animatorSet.start();
} else {
//取消點贊
--likeNumber;
setLikeNum();
ObjectAnimator handScaleAnim = ObjectAnimator.ofFloat(this, "handScale", 1f, 0.8f, 1f);
handScaleAnim.setDuration(duration);
handScaleAnim.start();
//手指上的四點消失,透明度設置爲0
setShingAlpha(0);
}
}
複製代碼
上面用了幾個動畫函數,這裏運用了茲定於屬性,上面代碼解釋很清楚了 動畫會觸發下面相應setXXXX()方法
/** * 手指縮放方法 * * @param handScale */
public void setHandScale(float handScale) {
//傳遞縮放係數
this.handScale = handScale;
//請求重繪View樹,即draw過程,視圖發生大小沒有變化就不會調用layout過程,而且重繪那些「須要重繪的」視圖
//若是是view就繪製該view,若是是ViewGroup,就繪製整個ViewGroup
invalidate();
}
/** * 手指上四點從0到1出現方法 * * @param shingAlpha */
public void setShingAlpha(float shingAlpha) {
this.shiningAlpha = shingAlpha;
invalidate();
}
/** * 手指上四點縮放方法 * * @param shingScale */
@Keep
public void setShingScale(float shingScale) {
this.shiningScale = shingScale;
invalidate();
}
/** * 設置數字變化 */
public void setLikeNum() {
//開始移動的Y座標
float startY;
//最大移動的高度
textMaxMove = SystemUtil.dp2px(getContext(), 20);
//若是點讚了 就下往上移
if (isLike) {
startY = textMaxMove;
} else {
startY = -textMaxMove;
}
ObjectAnimator textInAlphaAnim = ObjectAnimator.ofFloat(this, "textAlpha", 0f, 1f);
textInAlphaAnim.setDuration(duration);
ObjectAnimator textMoveAnim = ObjectAnimator.ofFloat(this, "textTranslate", startY, 0);
textMoveAnim.setDuration(duration);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(textInAlphaAnim, textMoveAnim);
animatorSet.start();
}
/** * 設置數值透明度 */
public void setTextAlpha(float textAlpha) {
this.textAlpha = textAlpha;
invalidate();
}
/** * 設置數值移動 */
public void setTextTranslate(float textTranslate) {
textMoveDistance = textTranslate;
invalidate();
}
/** * 畫出圓形波紋 * * @param shingCircleScale */
public void setShingCircleScale(float shingCircleScale) {
this.shingCircleScale = shingCircleScale;
invalidate();
}
/** * 圓形透明度設置 * * @param shingCircleAlpha */
public void setShingCircleAlpha(float shingCircleAlpha) {
this.shingCircleAlpha = shingCircleAlpha;
invalidate();
}
複製代碼
效果以下:
這個簡單例子對一些自定義View的基本使用都涉及了,如繪製,canvas的一些基本用法等。 和即刻點贊效果仍是有區別,能夠經過加下動畫差值器優化。 項目代碼:仿即刻點贊效果