目标效果查看:打开QQ->点击个人头像->点击个性名片->选择一个名片->选择效果二。
(仅限 vip及以上用户)分析效果
- 进入界面正常展示界面(中间头像,背景图片)
- 下滑界面依次进行以下动画:
- 整体高度增加及背景模糊化
- 头像周围显示光晕
- 标签从头像中心向周围扩散
- 标签在一定范围内随机移动
- 上滑恢复正常界面显示 同时进行以下动画:
- 整体高度缩小背景去模糊
- 头像光晕隐藏
- 标签向头像中心聚集并消失
- 标签可以拖动,当标签拖动时 头像光晕显示特定动画
- 标签拖动到头像显示区域时,头像光晕显示另一种特定动画
现在实现的效果
具体实现
分析实现需要的步骤
先分析布局类别分为三个类别:1.最外层的容器 2.气泡标签 3.中间的圆形头像。
下面开始对这三个部分分别进行实现
气泡标签(TagView)
首先对最简单的控件开始,这个控件为一个圆形背景及中间的文字显示区域展示,初看下来可以使用
TextView
来实现。不过TextView
不好进行测量,而且用到的功能也很简单,所以在这里使用自定义View
来实现对于相关文字的绘制使用StaticLayout
来辅助实现。
这里对于显示的文字需要做特殊处理,文字分为两个部分:标签名字(最多5个)显示及点赞数量。标签的整体的大小会根据tag中文字的数量来计算。在设置需要显示的信息时初始化StaticLayout
:
1 | void setSource(PresentationLayout.Tag tag){ |
测量时计算出文字区域所需大小及外层绘制圆形所需要的半径大小
1 |
|
绘制效果分为三个部分:圆形背景绘制、圆形边框绘制、文字绘制
1 | @Override |
圆形头像(PortraitWrapper)
显示圆形头像的控件很多了,这里使用一个ViewGroup 包裹对应控件 并负责绘制光晕
由于需要知道内部控件显示的半径的大小 所以定义一个接口用来获取这个半径
1 | public interface Circle { |
这里圆形头像展示使用CircleImageView使用这个控件 实现Circle
接口(Circle
声明了getRadius()
用于获取半径)
1 | @Override |
这里需要在CircleImageview
绘制一层圆形边框及两层光晕,所以对应有3个半径,这里对这3个半径建立对应Property
方便动画修改半径
1 | Property<PortraitWrapper, Integer> mFirstHaloProperty = new Property<PortraitWrapper, Integer>(Integer.class, "firstHalo") { |
隐藏动画
1 | ObjectAnimator step1 = ObjectAnimator.ofInt(this, mFirstHaloProperty, mCircleChildRadius + mBorderWidth / 2); |
当标签拖动时的动画
1 | int increment = mHaloTotalWidth / 3; |
当标签拖动到头像中时的动画(这里会有一个颜色alpha值渐渐降低的动画)所以添加修改画笔颜色的Property
1 | Property<PortraitWrapper,Float> mPaintProperty = new Property<PortraitWrapper,Float>(Float.class,"paintAlpha"){ |
1 | int increment = mHaloTotalWidth/4; |
外层布局(PresentationLayout)
继承至
RelativeLayout
模糊背景及高度变化
为了支持高度变化的动画 添加方法
1 | public void setHeight(int height) { |
之后就可以获取高度的属性动画:
1 | ObjectAnimator step1 = ObjectAnimator.ofInt(this, "height", mCollapsedHeight, mExpandHeight); |
对于背景模糊 是通过在背景之上添加已经模糊之后的图片 并通过修改模糊图片的Alpha值来实现动画效果
1 | /** |
在初始化之后就View.post(mDoBlurRunnable)
模糊图片。
模糊图片代码:
1 | /** |
标签气泡展示及隐藏
标签的展示及隐藏使用路径动画来实现 路径为二阶Bezier曲线。显示动画 出发点为头像中心点 终点为标签最终要移动到的位置。所有的标签移动到一个圆形的范围中,最多支持7个标签,这7个标签所对应的弧度:
1 | private final static double[] RADIUS = new double[]{0.7d, 2.25d, 3.14d, 5.5d, 0.1d, 1.4d, 4.7d}; |
隐藏则反之。
控制点,显示状况下。为了保证曲线的一致性 在标签显示的圆 内部圆 并加上弧度的偏移量计算对应的控制点。隐藏状况下,由于中心点的移动不能使用内部圆的方式保证曲线的一致性,所以单独进行计算(隐藏效果和QQ不太一致)
显示情况下计算控制点:
1 | controlX = Math.round(centerX + mInnerRadius * Math.cos(RADIUS[i] + CONTROL_RADIANS_OFFSET)); |
隐藏情况下计算控制点:
1 | //将centerX 和 centerY 看做坐标原点 |
对于AndroidL 以上可以直接使用 path动画:
1 | pathAnimator = ObjectAnimator.ofObject(tagView, mTagViewProperty, null, path); |
对于AnddroidL 以下使用 PathMeasure 帮助进行path动画
1 | pathAnimator = ObjectAnimator.ofObject(tagView, mTagViewProperty, new PathEvaluator(path), new PointF()); |
TagViewProperty 用于调用TagView 的 setPoint 修改TagView的位置
1 | void setPoint(PointF pointF){ |
对于path 转换为pointF 使用 PathEvaluator 来实现(内部通过PathMeasure计算Path)
1 | public class PathEvaluator implements TypeEvaluator<PointF> { |
标签气泡的移动
1 | /** |
标签气泡的拖拽(使用ViewDragHelper)
由于移动的TagView是同过改变TranslationX 及 TranslationY来移动,而ViewDragHelper 是通过 left top right bottom 判断是否点击到相应的View,所以这里不去捕获View
1 |
|
之后自行处理触摸事件并主动捕获View:
1 |
|
1 | public TagView findTopTagViewUnder(float x,float y){ |