跳至内容
← 所有文章

仿QQ个人信息展示页气泡标签效果

文章复现 QQ 个人信息页的气泡标签交互,将界面拆成 TagView、PortraitWrapper 和 PresentationLayout,分别负责标签绘制、圆形头像光晕与整体动画。 实现涵盖下拉展开与上滑收起、背景模糊和高度变化、二阶贝塞尔路径上的标签扩散与聚拢,以及随机移动和拖拽交互。文中还记录了 Android L 前后的路径动画适配,并针对标签通过 Translation 移动后与 ViewDragHelper 命中判断不一致的问题,改为自行处理触摸并主动捕获标签。

目标效果查看:打开QQ->点击个人头像->点击个性名片->选择一个名片->选择效果二。
notion image
(仅限 vip及以上用户)

分析效果

  • 进入界面正常展示界面(中间头像,背景图片)
  • 下滑界面依次进行以下动画:
    • 整体高度增加及背景模糊化
    • 头像周围显示光晕
    • 标签从头像中心向周围扩散
  • 标签在一定范围内随机移动
  • 上滑恢复正常界面显示 同时进行以下动画:
    • 整体高度缩小背景去模糊
    • 头像光晕隐藏
    • 标签向头像中心聚集并消失
  • 标签可以拖动,当标签拖动时 头像光晕显示特定动画
  • 标签拖动到头像显示区域时,头像光晕显示另一种特定动画

现在实现的效果

notion image

具体实现

分析实现需要的步骤

先分析布局类别分为三个类别:1.最外层的容器 2.气泡标签 3.中间的圆形头像。 下面开始对这三个部分分别进行实现

气泡标签(TagView)

首先对最简单的控件开始,这个控件为一个圆形背景及中间的文字显示区域展示,初看下来可以使用TextView来实现。不过TextView 不好进行测量,而且用到的功能也很简单,所以在这里使用自定义View来实现对于相关文字的绘制使用StaticLayout来辅助实现。
这里对于显示的文字需要做特殊处理,文字分为两个部分:标签名字(最多5个)显示及点赞数量。标签的整体的大小会根据tag中文字的数量来计算。在设置需要显示的信息时初始化StaticLayout:
测量时计算出文字区域所需大小及外层绘制圆形所需要的半径大小
绘制效果分为三个部分:圆形背景绘制、圆形边框绘制、文字绘制

圆形头像(PortraitWrapper)

显示圆形头像的控件很多了,这里使用一个ViewGroup 包裹对应控件 并负责绘制光晕
由于需要知道内部控件显示的半径的大小 所以定义一个接口用来获取这个半径
这里圆形头像展示使用CircleImageView使用这个控件 实现Circle接口(Circle 声明了getRadius()用于获取半径)
这里需要在CircleImageView绘制一层圆形边框及两层光晕,所以对应有3个半径,这里对这3个半径建立对应Property方便动画修改半径
显示动画
隐藏动画
当标签拖动时的动画
当标签拖动到头像中时的动画(这里会有一个颜色alpha值渐渐降低的动画)所以添加修改画笔颜色的Property

外层布局(PresentationLayout)

继承自RelativeLayout

模糊背景及高度变化

为了支持高度变化的动画 添加方法
之后就可以获取高度的属性动画:
对于背景模糊 是通过在背景之上添加已经模糊之后的图片 并通过修改模糊图片的Alpha值来实现动画效果
在初始化之后就View.post(mDoBlurRunnable) 模糊图片。 模糊图片代码:

标签气泡展示及隐藏

标签的展示及隐藏使用路径动画来实现 路径为二阶Bezier曲线。显示动画 出发点为头像中心点 终点为标签最终要移动到的位置。所有的标签移动到一个圆形的范围中,最多支持7个标签,这7个标签所对应的弧度:
隐藏则反之。 控制点,显示状况下。为了保证曲线的一致性 在标签显示的圆 内部圆 并加上弧度的偏移量计算对应的控制点。隐藏状况下,由于中心点的移动不能使用内部圆的方式保证曲线的一致性,所以单独进行计算(隐藏效果和QQ不太一致) 显示情况下计算控制点:
隐藏情况下计算控制点:
对于AndroidL 以上可以直接使用 path动画:
对于AndroidL 以下使用 PathMeasure 帮助进行path动画
TagViewProperty 用于调用TagView 的 setPoint 修改TagView的位置
对于path 转换为pointF 使用 PathEvaluator 来实现(内部通过PathMeasure计算Path)

标签气泡的移动

标签气泡的拖拽(使用ViewDragHelper)

由于移动的TagView是通过改变TranslationX 及 TranslationY来移动,而ViewDragHelper 是通过 left top right bottom 判断是否点击到相应的View,所以这里不去捕获View
之后自行处理触摸事件并主动捕获View:

项目地址

Loading...