前言

Android表现快捷菜单的形式有很多种,比如使用PopupWindow弹出来的小弹窗,类似QQ的侧拉功能菜单,以及之前讲过的弧形菜单( Android 自定义弧形旋转菜单栏——卫星菜单),这次要实现的是一个比较酷炫的菜单效果,虽然适合使用的场景可能不如前几种,但是整体动画效果还是蛮不错的,如下:

YRoundelMenu.gif

实现

思路

由于我们是作为一个菜单的形式,所以可以采用继承ViewGroup来作为一个容器,每个菜单子项都是一个子View的形式,展开和收缩动画可以采用属性动画的进度动态修改圆的半径。图标的排列需要考虑到各种数量情况下(1,2,3,4,5,6),能够平分圆周布局,可以通过计算圆弧内圈和外圈中间的弧线长度,再除以子View的数量得到每个子View的坐标即可。主要步骤和实现方式如下:

1.绘制内外圆圈,通过属性动画实现展开和收缩,以及颜色的渐变

2.通过PathMeasure计算圆周的长度,除以子View,计算每个子View在圆环中的坐标

3.子View的出场动画,通过调用setStartDelay实现间隔浮现效果

4.onTouchEvent中通过判断点击的区域处理点击事件,实现点击时展开或收缩

5.中心按钮旋转,添加控件阴影

效果截图

1.绘制内外圆圈,通过属性动画实现展开和收缩以及颜色的渐变

一共需要绘制两个圆,一个负责展示中心圆圈部分,一个负责展示外圈的菜单子项。

首先初始化两个状态下我们需要的画笔参数,这里mCenterPaint负责绘制中心部分,mRoundPaint 负责绘制展开后后面的大圆圈:

private Paint mCenterPaint;

private Paint mRoundPaint;

//收缩状态时的颜色 / 展开时外圈的颜色

private int mRoundColor;

//展开时中心圆圈的颜色

private int mCenterColor;

public void init(){

mCenterPaint= new Paint(Paint.ANTI_ALIAS_FLAG);

mCenterPaint.setColor(mRoundColor);

mCenterPaint.setStyle(Paint.Style.FILL);

mRoundPaint= new Paint(Paint.ANTI_ALIAS_FLAG);

mRoundPaint.setColor(mRoundColor);

mRoundPaint.setStyle(Paint.Style.FILL);

setWillNotDraw(false);

}

这里有个地方要注意,由于是自定义ViewGroup,因此要调用setWillNotDraw(false),否则我们调用invalidate的时候将不会触发onDraw。(具体原因可看ViewGroup的initViewGroup方法和mPrivateFlags标志位,ViewGroup在调用onDraw方法前做了判断)

接着初始化属性动画器:

mExpandAnimator = ValueAnimator.ofFloat(0, 1);

mExpandAnimator.setInterpolator(new OvershootInterpolator());

mExpandAnimator.setDuration(400);

mExpandAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

expandProgress = (float)animation.getAnimatedValue();

mRoundPaint.setAlpha((int) (expandProgress * 255));

invalidate();

}

});

mColorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), mRoundColor, mCenterColor);

mColorAnimator.setDuration(400);

mColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

mCenterPaint.setColor((Integer) animation.getAnimatedValue());

}

});

1)mExpandAnimator负责动态改变大圆圈的半径和透明度,采用OvershootInterpolator,让它有一种向外快速弹出一定值后再回到原来位置的弹性效果。用一个expandProgress记录当前的进度值,后面onDraw绘制的时候会派上用场。

2)mColorAnimator负责颜色的渐变,采用ArgbEvaluator颜色插值器,实现颜色值的过渡,在动画监听中设置给画笔。

接着在onDraw中根据刚才的动画值进行绘制:

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

//绘制放大的圆

if (expandProgress > 0) {

canvas.drawCircle(center.x, center.y, collapsedRadius + (expandedRadius - collapsedRadius) * expandProgress, mRoundPaint);

}

//绘制中间圆

canvas.drawCircle(center.x, center.y, collapsedRadius, mCenterPaint);

}

collapsedRadius 代表完全收缩状态下的圆圈半径,expandedRadius 代表完全展开状态下的圆圈半径。

通过drawCircle绘制两个圆,可以理解为其实是两个圆圈叠加在一块,一旦展开或者收缩,其中一个会发生颜色的渐变(刚才的颜色动画回调里不断给mCenterPaint设置新的过渡颜色),另一个的半径会在collapsedRadius和expandedRadius之间变化。

展开过程中,由一开始的collapsedRadius逐渐变化为expandedRadius

收缩过程中,由一开始的expandedRadius逐渐变化为collapsedRadius

绘制内外圆圈.gif

2.计算每个子View在圆环中的坐标

我们想要实现的效果是子View均匀排列在外围圆环中,那么这些子View的圆心必定刚好处在内外环中间的圆环线上,如下图虚线处:

计算虚线圆圈的半径示意图

红色代表最外围的圆的半径,蓝色代表中心圆圈的半径,那么虚线圆的半径便可以通过如下公式计算得出:

float radius = (expandedRadius - collapsedRadius) / 2 + collapsedRadius;

从而可以得到这个虚圆的路径:

RectF area = new RectF(

center.x - radius,

center.y - radius,

center.x + radius,

center.y + radius);

Path path = new Path();

path.addArc(area, 0, 360);

再通过PathMeasure测量圆的长度,结合子View的数量,得到每个子View之间的间距:

PathMeasure measure = new PathMeasure(path, false);

//测量圆的总长度

float len = measure.getLength();

//子菜单数量

int count = getChildCount();

//每个菜单之间的间距

float itemLength = len / count;

利用PathMeasure的getPosTan计算每个子View的坐标:

for (int i = 0; i < getChildCount(); i++) {

float[] itemPoints = new float[2];

measure.getPosTan(i * itemLength, itemPoints, null);

View item = getChildAt(i);

item.setX((int) itemPoints[0] - itemWidth / 2);

item.setY((int) itemPoints[1] - itemWidth / 2);

}

getPosTan一共有三个参数,第一个表示距离起点的距离,此处可以根据下标与刚才计算出来的菜单之间的间距相乘,从而使其均匀分布,第二个参数即对应位置的点的坐标,会赋给itemPoints这个数组,第三个参数是用来获取对应位置的正切值,这个可以用来实现一些路径上的指向效果(例如纸飞机沿着某条Path移动,飞机头方向保持与路径平行),此处第三个参数不需要用到,可以为null。

然后由于要获取的是菜单项的左上角的坐标,所以需要减去菜单项的宽度的1/2,如下图:

子View坐标计算示意图

3.菜单子项的出场动画

为了让整个View的效果更加丰富,可以在我们展开菜单的时候,让菜单子项接二连三地浮现出来:

//每40ms浮现一个

int delay = 40;

for (int i = 0; i < getChildCount(); i++) {

getChildAt(i).animate()

.setStartDelay(delay)

.setDuration(400)

.alphaBy(0f)

.scaleXBy(0f)

.scaleYBy(0f)

.scaleX(1f)

.scaleY(1f)

.alpha(1f)

.start();

delay += mItemAnimIntervalTime;

}

遍历所有子View,然后间隔一定时间启动动画,改变子View的大小比例和透明度,使其从无到有。

4.根据点击区域做不同的响应

按照正常的逻辑,如果当前是收缩状态,则点击中心区域会展开。如果当前是展开状态,则触发收缩效果,除非此时点击的是子View区域,就不拦截事件,留给子View去消费。我们可以通过计算触摸点与中心点的距离,与内外圆圈半径做比较,来作为判断的依据。

计算两点之间的距离可以采用Math.sqrt来计算,其实就是勾股定理:

public static double getPointsDistance(Point a, Point b) {

int dx = b.x - a.x;

int dy = b.y - a.y;

return Math.sqrt(dx * dx + dy * dy);

}

然后在onTouchEvent中去判断:

@Override

public boolean onTouchEvent(MotionEvent event) {

Point touchPoint = new Point();

touchPoint.set((int) event.getX(), (int) event.getY());

int action = event.getActionMasked();

switch (action) {

case MotionEvent.ACTION_DOWN: {

//计算触摸点与中心点的距离

double distance = getPointsDistance(touchPoint, center);

if(state == STATE_EXPAND){

//展开状态下,如果点击区域与中心点的距离不处于子菜单区域,就收起菜单

if (distance > (collapsedRadius + (expandedRadius - collapsedRadius) * expandProgress)

|| distance < collapsedRadius) {

collapse();

return true;

}

//展开状态下,如果点击区域处于子菜单区域,则不消费事件

return false;

}else{

//收缩状态下,如果点击区域处于中心圆圈范围内,则展开菜单

if(distance < collapsedRadius){

expand();

return true;

}

//收缩状态下,如果点击区域不在中心圆圈范围内,则不消费事件

return false;

}

}

}

return super.onTouchEvent(event);

}

5.中心按钮旋转,添加控件阴影

中心按钮旋转可以在onDraw中直接利用画布的旋转来实现:

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

//绘制放大的圆

忽略部分代码...

//绘制中间圆

忽略部分代码...

//绘制中心图标

int count = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);

canvas.rotate(45*expandProgress, center.x, center.y);

mCenterDrawable.draw(canvas);

canvas.restoreToCount(count);

}

由于画布是ViewGroup的,因此直接旋转画布会对整个ViewGroup造成影响,我们想要的只是单单旋转中间按钮而已,因此通过saveLayer和restoreToCount来保证不影响其他部分的绘制,在它们的里面执行canvas.rota,由于expandProgress是在[0,1]之间变化,所以我们让它的角度在0°~45°之间倾斜。

Android5.0之后View提供了一个新的特性elevation,使用它可以让View产生阴影效果:

if (Build.VERSION.SDK_INT >= 21) {

setElevation(8);

}

单纯设置elevation还不够,需要为它指定一个轮廓,即搭配ViewOutlineProvider来使用,先自定义一个ViewOutlineProvider,重写它的getOutline,里面定义轮廓的形状和大小区域:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)

public class OvalOutline extends ViewOutlineProvider {

public OvalOutline() {

super();

}

@Override

public void getOutline(View view, Outline outline) {

int radius = (int) (collapsedRadius + (expandedRadius - collapsedRadius) * expandProgress);

Rect area = new Rect(

center.x - radius,

center.y - radius,

center.x + radius,

center.y + radius);

outline.setRoundRect(area, radius);

}

}

然后将其设置给我们的ViewGroup,记得加上5.0以上的判断。

@Override

protected void onSizeChanged(int w, int h, int oldw, int oldh) {

super.onSizeChanged(w, h, oldw, oldh);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

setOutlineProvider(new OvalOutline());

}

}

结语

整体效果还是蛮不错的,虽然使用场景可能有点局限,比如在一些列表里点击编辑的时候可以展开,或者是一些悬浮球快捷操作的场景等等,另外还可以加上一些后续的交互,比如手动旋转轮盘的效果,完整代码已上传到 一个集合酷炫效果的自定义组件库,欢迎Issue。

欢迎关注 Android小Y 的简书,更多Android精选自定义View

GitHub:GitHub-ZJYWidget

CSDN博客:IT_ZJYANG

简 书:Android小Y

在 GitHub 上建了一个集合炫酷自定义View的项目,里面有很多实用的自定义View源码及demo,会长期维护,欢迎Star~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~

android 弹出菜单环形,『Android自定义View实战』实现一个小清新的弹出式圆环菜单...相关推荐

  1. android 行布局选择器,『自定义View实战』—— 银行种类选择器

    在工作中难免遇到自定义 View 的相关需求,本身这方面比较薄弱,因此做个记录,也是自己学习和成长的积累.自定义View实战 前言 年前的最后一个开发需求,将之前H5开卡界面转变成native.意思就 ...

  2. android 选择银行类型,『自定义View实战』—— 银行种类选择器

    在工作中难免遇到自定义 View 的相关需求,本身这方面比较薄弱,因此做个记录,也是自己学习和成长的积累.自定义View实战 前言 年前的最后一个开发需求,将之前H5开卡界面转变成native.意思就 ...

  3. android 图片处理过程中添加进度条,『Android自定义View实战』给我一个图标,还你一个水波纹进度球...

    前言 我们都知道,平时表现进度的方式有千千万万种(没有UI想不到的,只有你做不到的= =.),其中有一种就是水波纹进度球的形式,网上很多种实现都是直接采用纯色填充的方式,即水波纹都是纯颜色填充,效果看 ...

  4. android 画圆弧动画,『Android自定义View实战』自定义带入场动画的弧形百分比进度条...

    写在前面 这是在简书发表的处女座,这个想法也停留在脑海中很久了,一直拖到现在(懒癌发作2333),先自我介绍一番,一枚刚毕业不久的Android程序猿,初出茅庐的Android小生,之前一直在CSDN ...

  5. 『自定义View实战』—— 仿ios图标下载view DownloadLoadingView

    2019独角兽企业重金招聘Python工程师标准>>> ## 前言 最近项目需要接入环信客服 SDK ,我配合这同事完成,其中我负责文件下载这部分. 因为时间比较紧张,8 天的时间完 ...

  6. [自定义控件]android自定义view实战之太极图

    android自定义view实战之太极图 尊重原创,转载请注明出处: http://blog.csdn.net/qq137722697 自定义view是Android工程师进阶不可避免要接触的,我的学 ...

  7. Android绘制竖直虚线完美解决方案—自定义View

    Android绘制竖直虚线完美解决方案-自定义View 开发中我们经常会遇到绘制虚线的需求,一般我们使用一个drawable文件即可实现,下面我会先列举常规drawable文件的实现方式. 使用dra ...

  8. 【Android自定义View实战】之自定义评价打分控件RatingBar,可以自定义星星大小和间距...

    [Android自定义View实战]之自定义评价打分控件RatingBar,可以自定义星星大小和间距

  9. Android 系统(201)---Android 自定义View实战系列 :时间轴

    Android 自定义View实战系列 :时间轴 Android开发中,时间轴的 UI需求非常常见,如下图: 本文将结合 自定义View & RecyclerView的知识,手把手教你实现该常 ...

最新文章

  1. python从入门到实践_Python编程从入门到实践日记Day32
  2. Java类集框架 —— LinkedList源码分析
  3. python下划线怎么输入_python长的下划线怎么打
  4. Spring Cloud(F版)搭建高可用服务注册中心
  5. C++(13)--函数的进阶:内联、传递引用、参数默认值、重载、函数模板
  6. Linux io内存存在的意义~
  7. Node 实现 AES 加密,结果输出为“byte”。
  8. ASP.Net七大内置对象 (整理的不错,转过来参考)
  9. C语言正交表测试用例,正交表设计用例(简单+实用) - Jackc的个人空间 - 51Testing软件测试网 51Testing软件测试网-软件测试人的精神家园...
  10. adb安装apk到智能TV上
  11. 男女逗段,瞅瞅有没有说到你
  12. win10玩cf如何调全屏_穿越火线:WIN10系统烟雾头和画面卡顿解决办法
  13. 2022-7-6-18
  14. android m3u8 合并,M3u8合并APP
  15. 计算机必须设置默认打印机,win10系统禁止更改默认打印机设置的还原技巧
  16. WCF基础教程(三)——WCF通信过程及配置文件解析
  17. Android PowerManager 进入屏保、睡眠的过程梳理
  18. 两条命令彻底修复动态链接库
  19. 桌面的此电脑图标变成了快捷方式如何解决?
  20. maya2018的uv导入和导出

热门文章

  1. linux ubuntu桌面下载,Lubuntu 20.04 LTS 发布下载,LXQt为默认桌面
  2. 服务器vrrp协议,VRRP协议、Keepalived
  3. python登录网页后打印_python爬虫中文网页cmd打印出错问题解决
  4. RapidScada 应用---安卓手机端监控插件KpAndroid
  5. Uni-app “悦读”项目开发
  6. 一些对数学领域及数学研究的个人看法(转载自博士论坛wcboy)
  7. 各浏览器性能指标测试
  8. ajax.then()用法,使用es6的then()方法封装jquery的ajax请求
  9. 语音合成(speech synthesis)方向一:双重学习Dual Learning
  10. midjourney starter入门笔记:当代的神笔马良