今天朋友看了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的一些基本用法等。 和即刻点赞效果仍是有区别,能够经过加下动画差值器优化。 项目代码:仿即刻点赞效果