AnimatedPathView实现自定义图片标签
老早用过小红书app,对于他们客户端笔记这块的设计非常喜欢,恰好去年在小红书的竞争对手公司,公司基于产品的考虑和产品的发展,也需要将app社交化,于是在社区分享这块多多少少参照了小红书的设计,这里面就有一个比较有意思的贴纸,标签等设计,这里用到了GpuImage的库,这个demo我也将代码开源了,有需要的去fork我的github的代码,今天要说的是详情页面的AnimatedPathView实现可以动起来的标签。(之前我们项目中由于时间问题,将这种效果用h5实现了,不过现在回React Native之后,发现实现起来更简单了),今天要说的是用android实现这种效果。
且看个效果图:
要实现我们这样的效果,首先分析下,线条的绘制和中间圆圈的实现,以及文字的绘制。
对于线条的绘制我们不多说,直接canvas.DrawLine,不过这种线条是死的,不能实现运动的效果,还好Java为我们提供了另一个方法,我们可以用Path去实现,之前做腾讯手写板的时候也是这么做的(可以点击链接查看效果,不过代码没办法公开),点击打开链接,通过上面说的,我们改变PathEffect的偏移量就可以改变path显示的长度,从而实现动画的效果。而PathEffect有很多子类,从而满足不同的效果,这里不再说明。
float percentage = 0.0f; PathEffect effect = new DashPathEffect(new float[]{pathLength, pathLength}, pathLength - pathLength*percentage);
这里贴出AnimatedPathView的完整代码:
public class AnimatedPathView extends View { private Paint mPaint; private Path mPath; private int mStrokeColor = Color.parseColor("#ff6c6c"); private int mStrokeWidth = 8; private float mProgress = 0f; private float mPathLength = 0f; private float circleX = 0f; private float circleY = 0f; private int radius = 0; private String pathText="化妆包..."; private int textX,textY; public AnimatedPathView(Context context) { this(context, null); init(); } public AnimatedPathView(Context context, AttributeSet attrs) { this(context, attrs, 0); init(); } public AnimatedPathView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AnimatedPathView); mStrokeColor = a.getColor(R.styleable.AnimatedPathView_pathColor, Color.parseColor("#ff6c6c")); mStrokeWidth = a.getInteger(R.styleable.AnimatedPathView_pathWidth, 8); a.recycle(); init(); } private void init() { mPaint = new Paint(); mPaint.setColor(mStrokeColor); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(mStrokeWidth); mPaint.setAntiAlias(true); setPath(new Path()); } public void setPath(Path p) { mPath = p; PathMeasure measure = new PathMeasure(mPath, false); mPathLength = measure.getLength(); } public void setPathText(String pathText,int textX,int textY ) { this.pathText=pathText; this.textX=textX; this.textY=textY; } public void setPath(float[]... points) { if (points.length == 0) throw new IllegalArgumentException("Cannot have zero points in the line"); Path p = new Path(); p.moveTo(points[0][0], points[0][1]); for (int i = 1; i < points.length; i++) { p.lineTo(points[i][0], points[i][1]); } //将第一个xy坐标点作为绘制的原点 circleX = points[0][0] - radius / 2; circleY = points[0][1] - radius / 2; setPath(p); } public void setPercentage(float percentage) { if (percentage < 0.0f || percentage > 1.0f) throw new IllegalArgumentException("setPercentage not between 0.0f and 1.0f"); mProgress = percentage; invalidate(); } public void scalePathBy(float x, float y) { Matrix m = new Matrix(); m.postScale(x, y); mPath.transform(m); PathMeasure measure = new PathMeasure(mPath, false); mPathLength = measure.getLength(); } public void scaleCircleRadius(int radius) { this.radius = radius; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制圆形 // drawCircle(canvas); //绘线条 drawPathEffect(canvas); //绘制文字 drawText(canvas); canvas.restore(); } private void drawText(Canvas canvas) { mPaint.setTextSize(28); mPaint.setColor(Color.parseColor("#ffffff")); if (canvas!=null&& !TextUtils.isEmpty(pathText)){ canvas.drawText(pathText,textX,textY,mPaint); } invalidate(); } private void drawPathEffect(Canvas canvas) { PathEffect pathEffect = new DashPathEffect(new float[]{mPathLength, mPathLength}, (mPathLength - mPathLength * mProgress)); mPaint.setPathEffect(pathEffect); mPaint.setStrokeWidth(4); mPaint.setColor(Color.parseColor("#ffffff")); canvas.save(); canvas.translate(getPaddingLeft(), getPaddingTop()); canvas.drawPath(mPath, mPaint); } private void drawCircle(Canvas canvas) { int strokenWidth = 25; mPaint.setStrokeWidth(strokenWidth); mPaint.setColor(Color.parseColor("#ffffff")); canvas.drawCircle(circleX, circleY, radius , mPaint); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(widthMeasureSpec); int measuredWidth, measuredHeight; if (widthMode == MeasureSpec.AT_MOST) throw new IllegalStateException("AnimatedPathView cannot have a WRAP_CONTENT property"); else measuredWidth = widthSize; if (heightMode == MeasureSpec.AT_MOST) throw new IllegalStateException("AnimatedPathView cannot have a WRAP_CONTENT property"); else measuredHeight = heightSize; setMeasuredDimension(measuredWidth, measuredHeight); } }
这段代码借鉴了点击打开链接的部分代码,并在此基础上做了更多的判断和改变,以满足本文开头说说的那种需要,上面的代码只是实现了画线条的效果,那么如何实现中间圆圈的闪烁呢,其实也很简单,我们可以用动画来实现(View动画),这里我们大可以自己自定义一个View实现,而这个View包含了圆圈闪烁和画线,按照上面的逻辑我们写一个自定义的View,代码如下:
public class PointView extends FrameLayout { private Context mContext; private List<PointScaleBean> points; private FrameLayout layouPoints; private AnimatedPathView animatedPath; private int radius=10; private String text="图文标签 $99.00"; public PointView(Context context) { this(context, null); } public PointView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PointView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context, attrs); } private void initView(Context context, AttributeSet attrs) { this.mContext = context; View imgPointLayout = inflate(context, R.layout.layout_point, this); layouPoints = (FrameLayout) imgPointLayout.findViewById(R.id.layouPoints); animatedPath=(AnimatedPathView) imgPointLayout.findViewById(R.id.animated_path); } public void addPoints(int width, int height) { addPoint(width, height); } public void setPoints(List<PointScaleBean> points) { this.points = points; } private void addPoint(int width, int height) { layouPoints.removeAllViews(); for (int i = 0; i < points.size(); i++) { double width_scale = points.get(i).widthScale; double height_scale = points.get(i).heightScale; LinearLayout view = (LinearLayout) LayoutInflater.from(mContext).inflate(R.layout.layout_img_point, this, false); ImageView imageView = (ImageView) view.findViewById(R.id.imgPoint); imageView.setTag(i); AnimationDrawable animationDrawable = (AnimationDrawable) imageView.getDrawable(); animationDrawable.start(); LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); layoutParams.leftMargin = (int) (width * width_scale); layoutParams.topMargin = (int) (height * height_scale); // imageView.setOnClickListener(this); layouPoints.addView(view, layoutParams); } initView(); initPathAnimated(); } private void initPathAnimated() { ViewTreeObserver observer = animatedPath.getViewTreeObserver(); if(observer != null){ observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { animatedPath.getViewTreeObserver().removeGlobalOnLayoutListener(this); animatedPath.scaleCircleRadius(radius); animatedPath.scalePathBy(animatedPath.getWidth()/2,animatedPath.getHeight()/2); float[][] points = new float[][]{ {animatedPath.getWidth()/2-radius/2,animatedPath.getHeight()/2-radius/2}, {animatedPath.getWidth()/2- UIUtils.dp2px(mContext,30), animatedPath.getHeight()/2- UIUtils.dp2px(mContext,40)}, {animatedPath.getWidth()/2-UIUtils.dp2px(mContext,150), animatedPath.getHeight()/2- UIUtils.dp2px(mContext,40)}, }; animatedPath.setPath(points); // animatedPath.setPathText(text,animatedPath.getWidth()/2-UIUtils.dp2px(mContext,150), animatedPath.getHeight()/2- UIUtils.dp2px(mContext,50)); } }); } } private void initView() { animatedPath.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { ObjectAnimator anim = ObjectAnimator.ofFloat(view, "percentage", 0.0f, 1.0f); anim.setDuration(2000); anim.setInterpolator(new LinearInterpolator()); anim.start(); } }); } }
上面对应的布局和资源文件:
layou_point.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" > <com.yju.app.widght.path.AnimatedPathView android:id="@+id/animated_path" android:layout_width="match_parent" android:layout_height="wrap_content" /> <FrameLayout android:id="@+id/layouPoints" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" /> </FrameLayout>
layout_img_point.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"> <ImageView android:id="@+id/imgPoint" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/point_img" /> </LinearLayout>
文中用到的Anim就是帧动画了,
<?xml version="1.0" encoding="utf-8"?> <animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false"> <item android:drawable="@drawable/point_img1" android:duration="100" /> ....省略n多图片资源 <item android:drawable="@drawable/point_img13" android:duration="100" /> </animation-list>
而最后我们只需要在我们自己的MainActivity中添加简单的代码既可实现上面的效果:
private void initPointView() { List<PointScaleBean> list=new ArrayList<>(); PointScaleBean point=new PointScaleBean(); point.widthScale = 0.36f; point.heightScale = 0.75f; list.add(point); pointView.setPoints(list); pointView.addPoints(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT); }
对于布局我是这么做的,将View的父布局的背景加一个图片,实际的开发中大家可以写一个相对的布局,这个就能实现实时的效果了,好了就写到这里,有疑问请留言或者加群(278792776)。
附件:一个滤镜效果:点击打开链接
AnimatedPathView实现自定义图片标签的更多相关文章
- CKEditor5 + vue2.0 自定义图片上传、highlight、字体等用法
因业务需求,要在 vue2.0 的项目里使用富文本编辑器,经过调研多个编辑器,CKEditor5 支持 vue,遂采用.因 CKEditor5 文档比较少,此处记录下引用和一些基本用法. CKEdit ...
- DedeCMS织梦自定义图片字段调用出现{dede:img ..}
做站过程中碰到这样一个问题,找到解决办法收藏分享:为什么在首页用自定义列表调用出来的图片字段不是正确的图片地址,而是类似于: {dede:img text='' width='270' height= ...
- TensorFlow2.0(10):加载自定义图片数据集到Dataset
.caret, .dropup > .btn > .caret { border-top-color: #000 !important; } .label { border: 1px so ...
- [19/06/04-星期二] HTML基础_实体(转义字符)、图片标签(img)、元标签(meta)、语法规范、内联框架(iframe)、超链接
一.实体(转义字符) 在HTML中,一些诸如<.> 就是普通的小于号和大于号不能直接使用,因为浏览可能会把它当成一个标签去解析,所以需要一些特殊字符去表示这些特殊字符, 这些字符我们称他们 ...
- [19/05/14-星期二] HTML_body标签(列表标签和图片标签)
一.列表标签 <!-- 快捷键 1.<meta charset="UTF-8"/> 用m6可直接写出 2.复制当前1行到下一行 ctrl+shift+R --&g ...
- DEDE建站之图片标签技巧指南
做dede站的时候,曾经遇到很苦恼的事情,就是给图片集添加了一个网上下载下来的特效,需要给图片的链接上添加一个rel属性,供JS调用以达到那种特效.但是当时只知道dede的图片链接标签是[field: ...
- 使用自定义tld标签简化jsp的繁琐操作
最近做一个树形结构的展示,请求目标页面后,后台只返回简单的List,虽然有想过在jsp页面内做一些操作简化,但是太繁琐了,其他的标签又不能满足需求,所以只能自己做一个.使用tld标签可以简化jsp代码 ...
- php正则获取html图片标签信息(采集图片)
php获取html图片标签信息(采集图片),实现图片采集及其他功能,带代码如下: <?php $str="<img src='./a.jpg'/>111111<img ...
- 网页qq客服代码并自定义图片
<script>var online= new Array();</script> <script src="http://webpresence.qq.com ...
随机推荐
- $.messager.confirm 用法
<script type="text/javascript"> $(function () { $.messager.defaults = { ...
- 浅谈Log4net在项目中如何记录日志
一 引入背景 在软件开发周期中,无论是开发中,或是测试中,或是上线后,选择合适的工具监控程序的运行状态至关重要,只有如此,才能更好地排查程序问题和检测程序性能问题等.本篇文章主要与大家分享,如何 ...
- numpy的初探
# data = numpy.genfromtxt("C:\\Users\\Admin\Desktop\\111.txt", delimiter='\t', dtype='str' ...
- 读书学习-Python--描述符(python3)
转自我的知乎文章(https://zhuanlan.zhihu.com/p/32487852) 何为描述符?描述符就是实现了__get__.__set__和__delete__三者中任意一个的类.是用 ...
- 常用java开发文档链接
使用java开源工具httpClient及jsoup抓取解析网页数据 : https://blog.csdn.net/lovoo/article/details/52674712 jsoup Cook ...
- JS基础(二)
一.JS中的循环结构 循环结构的执行步骤 1.声明循环变量: 2.判断循环条件: 3.执行循环体操作: 4.更新循环变量 然后,循环执行2-4,直到条件不成立时,跳出循环. while循环()中的表达 ...
- Vue nextTick 机制
背景 我们先来看一段Vue的执行代码: export default { data () { return { msg: 0 } }, mounted () { this.msg = 1 this.m ...
- RedHatEnterpriseLinuxServerRelease7.3上配置vsftp服务器
1.vsftpd 服务启停相关命令 systemctl start vsftpd systemctl stop vsftpd systemctl restart vsftpd 2.配置文件/etc/v ...
- linux和android开发链接
1.Tracy Mcgrady的专栏冰山一角:linux和Android底层开发,主要是mtk系列点击打开链接 2.郁闷Wednesday:嵌入式linux 单片机 android,点击打开链接 3. ...
- 关于 linux中TCP数据包(SKB)序列号的小笔记
关于 SKB序列号的小笔记 为了修改TCP协议,现在遇到了要改动tcp分组的序列号,但是只是在tcp_sendmsg函数中找到了SKB的end_seq 一直没有找到seq 不清楚在那里初始化了,就 ...