内容简介:今天朋友看了HenCoder的自定义View后说,HenCoder对自定义View讲的不错。实践中仿写即刻的点赞你有思路吗,你不实现一下?二话不说,看了朋友手机效果,对他说:实现不难,用到了位移,缩放,渐变动画和自定义View的基础用法,好,那我实现一下,刚好加深对自定义View的理解。把即刻app下载后,以解压包的方式解压,发现点赞效果有三张图,一张是没有点赞的小手图片,一张是点赞后的红色小手图片,最后一张是点赞后,点赞手指上的四点如下图:先仔细看上面即刻的点赞效果图,点赞后:灰色小手缩小了一下,并消失
今天朋友看了HenCoder的自定义View后说,HenCoder对自定义View讲的不错。实践中仿写即刻的点赞你有思路吗,你不实现一下?二话不说,看了朋友手机效果,对他说:实现不难,用到了位移,缩放,渐变动画和自定义View的基础用法,好,那我实现一下,刚好加深对自定义View的理解。
素材准备
把即刻app下载后,以解压包的方式解压,发现点赞效果有三张图,一张是没有点赞的小手图片,一张是点赞后的红色小手图片,最后一张是点赞后,点赞手指上的四点如下图:
实践思路
效果图
先仔细看上面即刻的点赞效果图,点赞后:灰色小手缩小了一下,并消失变成红色小手,红色小手放大了一些,并且手指上有四点出现,下面描述四点图像或者高亮都是指它。另外中间有一圈淡红色的圆形扩散放大效果,右边的数是一个一个字符第跳动,并不是整个数一起在跳动中被新的数替换掉,好像是数字轮表。如上面:3往上移并渐渐消失,4从下面出来并渐渐清晰出现。取消点赞后:高亮的四点消失,红色小手变成灰色小手,字符往下移,整个效果和点赞效果相反,下面准备用一个View来完成以上的实现。
具体分析
下面确定小手上的四点位置,因为在Android上有坐标系这个概念,画出一张图只要确定左上角就可以了。
小手的和手指上的四点位置绘制了,下面绘制数字文本,因为数字文本比较特殊,打算将整形转换为String类型,再通过字符数组将数字一个一个绘制出来。
下面实际用法不同,因为坐标原点不在左上角 画完一个字符后,下一个字符的x坐标加上上一个字符的宽度即可,高度是不变的,所以不用管。 点赞过程和取消点赞再结合动画实现就可以了。 上面只是粗略的把图像元素定位了,下面具体实现:
具体实现
初始化
在values下的attrs文件下添加属性集合如下:
<?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文件下配置属性:
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方法是初始化一些画笔,文本显示范围
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对象,上面分析很清楚了,一个是小手上的四点,一个是点赞小手,最后一个是没点赞的小手。
计算宽高
/**
* 测量宽高
* 这两个参数是由父视图经过计算后传递给子视图
* @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,上面已经解释很清楚了。
绘制onDraw
绘制小手
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); 复制代码
前两个参数就是确定圆中心,我设置在小手图像中心。
触摸处理onTouchEvent
我是设置触摸就触发点赞事件:
@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的一些基本用法等。 和即刻点赞效果还是有区别,可以通过加下动画差值器优化。 项目代码: 仿即刻点赞效果
关注微信公众号,一面技术一面艺术
以上所述就是小编给大家介绍的《Android自定义View教你一步一步实现即刻点赞效果》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 自定义带动画效果的模态框
- 支持横向、竖向无限滚动和自定义指示器的广告条BannerView和淘宝头条效果
- jQuery效果—雪花飘落
- jQuery效果—雪花飘落
- SlidingMenu实现侧滑效果
- 鼠标悬停动画效果
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Impractical Python Projects
Lee Vaughan / No Starch Press / 2018-11 / USD 29.95
Impractical Python Projects picks up where the complete beginner books leave off, expanding on existing concepts and introducing new tools that you’ll use every day. And to keep things interesting, ea......一起来看看 《Impractical Python Projects》 这本书的介绍吧!