Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >自定义View实现Dribbble上动感的Gallery App Icon

自定义View实现Dribbble上动感的Gallery App Icon

作者头像
Jingbin
发布于 2022-01-05 06:55:00
发布于 2022-01-05 06:55:00
65500
代码可运行
举报
文章被收录于专栏:Android 技术栈Android 技术栈
运行总次数:0
代码可运行

Gallery App Icon.gif

之前在dribbble看到一个很好看的动画效果,很想要,遂仿之。也为了练一下自定义控件,有段时间了,现在整理出来

dribbble地址:Gallery App Icon

思路

拆解一下,还是比较简单,需要绘制的有:

  • 圆形背景
  • 太阳(圆形)
  • 山(三角形)
  • 云朵(圆角矩形 + 三个圆)

需要进行的动画:

  • 太阳 - 旋转动画
  • 山 - 上下平移动画
  • 云朵 - 左右平移动画

不必绘制圆角外框,因为各个手机厂商的应用icon的圆角不一样,我们可以在Android Studio里生成应用图标。如果有必要也可以自己使用shape画出来。

其中难处是进行太阳的动画和绘制云朵,因为太阳的旋转动画需要计算旋转的圆上点的坐标,而云朵的形状是不规则的。

绘制

1.圆形背景

圆形.png

这里的白色圆角外框是shape画的,蓝色的圆形背景绘制也比较简单,主要是在onDraw()方法里使用canvas.drawCircle()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 将View切成圆形,否则绘制的山和云朵会出现在圆形背景之外
        mRoundPath.reset();
        mRoundPath.addCircle(mViewCircle, mViewCircle, mViewCircle, Path.Direction.CW);
        canvas.clipPath(mRoundPath);
        // 绘制圆形背景
        canvas.drawCircle(mViewCircle, mViewCircle, mViewCircle, mBackgroundPaint);
    }

这里的mViewCircle是指view的半径;mBackgroundPaint是用来画背景色的Paint。

mViewCircle获取:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 取宽高的最小值
        mParentWidth = mParentHeight = Math.min(getWidth(), getHeight());
        // View的半径
        mViewCircle = mParentWidth >> 1;
    }

mBackgroundPaint背景色设置一个颜色就好:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBackgroundPaint.setColor(mBackgroundColor);

其中如果不将View切成圆形会出现的情况为:

2.绘制太阳和进行旋转动画

如果是单纯画太阳的话,确定好x,y坐标和半径,然后加个颜色paint就好了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
canvas.drawCircle((mParentWidth / 2) - getValue(90), (mParentHeight / 2) - getValue(80), sunWidth / 2, mSunPaint);

但是我们要加上动画,这时候我们需要了解到:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * Transform the points in this path by matrix, and write the answer
 * into dst. If dst is null, then the the original path is modified.
 *
 * @param matrix The matrix to apply to the path
 * @param dst    The transformed path is written here. If dst is null,
 *               then the the original path is modified
 */
public void transform(Matrix matrix, Path dst) {
    long dstNative = 0;
    if (dst != null) {
        dst.isSimplePath = false;
        dstNative = dst.mNativePath;
    }
    nTransform(mNativePath, matrix.native_instance, dstNative);
}

该方法可以将一个path进行matrix转换,即矩阵转换,因此我们可以通过方法matrix.postTranslate来实现平移动画,即创建一个循环动画,通过postTranslate来设置动画值就可以了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * Postconcats the matrix with the specified translation. M' = T(dx, dy) * M
 * dx,dy,是x,y坐标移动的差值。
 */
public boolean postTranslate(float dx, float dy) {
    nPostTranslate(native_instance, dx, dy);
    return true;
}

我们先在onSizeChanged()里得到起始点太阳圆心的x,y坐标,然后再在onDraw()里实时获取要旋转时的x,y坐标,最后得到对应的差值。

onSizeChanged()里绘制太阳和得到旋转时起始点的x,y坐标:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private void drawSun() {
    // sun图形的直径
    int sunWidth = getValue(70);
    // sun图形的半径
    int sunCircle = sunWidth / 2;
    // sun动画半径 = (sun半径 + 80(sun距离中心点的高度) + 整个View的半径 + sun半径 + 20(sun距离整个View的最下沿的间距)) / 2
    mSunAnimCircle = (sunWidth + getValue(100) + mViewCircle) / 2;
    // sun动画的圆心x坐标
    mSunAnimX = mViewCircle;
    // sun动画的圆心y坐标 = sun动画半径 + (整个View的半径 - 80(sun距离中心点的高度) - sun半径)
    mSunAnimY = mSunAnimCircle + (mViewCircle - getValue(80) - sunCircle);
    // 得到圆形旋转动画起始点的x,y坐标,初始角度为-120
    mSunAnimXY = getCircleXY(mSunAnimX, mSunAnimY, mSunAnimCircle, -120);
    // 绘制sun
    mSunPath.addCircle(mSunAnimXY[0], mSunAnimXY[1], sunCircle, Path.Direction.CW);
}

其中稍微困难点的是得到圆上的x,y坐标 getCircleXY()

已知的条件:圆心O的坐标(mSunAnimX,mSunAnimY)、半径为sunCircle、角度angle = -120度。

(角度是相对于图中横线,顺时针为正,逆时针为负),要计算p点的坐标(x1,y1)有如下公式:

x1 = x0 + r * cos(angle * PI / 180)

y1 = y0 + r * sin(angle * PI /180)

其中angle* PI/180是将角度转换为弧度。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 求sun旋转时,圆上的点。起点为最右边的点,顺时针。
 * x1   =   x0   +   r   *   cos(a   *   PI  /180  )
 * y1   =   y0   +   r   *   sin(a   *   PI  /180  )
 *
 * @param angle         角度
 * @param circleCenterX 圆心x坐标
 * @param circleCenterY 圆心y坐标
 * @param circleR       半径
 */
private int[] getCircleXY(int circleCenterX, int circleCenterY, int circleR, float angle) {
    int x = (int) (circleCenterX + circleR * Math.cos(angle * Math.PI / 180));
    int y = (int) (circleCenterY + circleR * Math.sin(angle * Math.PI / 180));
    return new int[]{x, y};
}

然后我们在onDraw()里可动态得到圆上的其他点的x,y坐标达到旋转的效果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// x y 坐标
int[] circleXY = getCircleXY(mSunAnimX, mSunAnimY, mSunAnimCircle, mSunAnimatorValue);
mSunComputeMatrix.postTranslate(circleXY[0] - mSunAnimXY[0], circleXY[1] - mSunAnimXY[1]);
mSunPath.transform(mSunComputeMatrix, mSunComputePath);
canvas.drawPath(mSunComputePath, mSunPaint);

mSunAnimatorValue为变化的角度[-120,240]。这样就可以执行太阳的旋转动画:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * sun的动画
 */
private void setSunAnimator() {
    ValueAnimator mSunAnimator = ValueAnimator.ofFloat(-120, 240);
    mSunAnimator.setDuration(2700);
    mSunAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
    mSunAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mSunAnimatorValue = (float) animation.getAnimatedValue();
            invalidate();
        }
    });
    mSunAnimator.start();
}

3.山和上下平移动画

画了上面的太阳旋转动画后,这个就相对比较简单了,因为只涉及到纵坐标y的变化,x不会变,仔细观察会发现,y坐标会先向上移动然后再向下快速移动。

onSizeChanged()里绘制三座山和得到要平移的y坐标:

drawMou(mViewCircle, mViewCircle - getValue(10), getValue(10));

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 画中间的三座山
 *
 * @param x 中心点左坐标
 * @param y 中心点右坐标
 */
private void drawMou(int x, int y, int down) {
    // 左右山 Y坐标相对于中心点下移多少
    int lrmYpoint = down + getValue(30);
    // 左右山 X坐标相对于中心点左移或右移多少
    int lrdPoint = getValue(120);
    // 左右山 山的一半的X间距是多少
    int lrBanDis = getValue(140);
    // 中间山 山的一半的X间距是多少
    int lrBanGao = getValue(150);

    // 左山
    mLeftMountainPath.reset();
    // 起点
    mLeftMountainPath.moveTo(x - lrdPoint, y + lrmYpoint);
    mLeftMountainPath.lineTo(x - lrdPoint + lrBanDis, y + lrmYpoint + lrBanGao);
    mLeftMountainPath.lineTo(x - lrdPoint - lrBanDis, y + lrmYpoint + lrBanGao);
    // 使这些点构成封闭的多边形
    mLeftMountainPath.close();

    // 右山
    mRightMountainPath.reset();
    mRightMountainPath.moveTo(x + lrdPoint + getValue(10), y + lrmYpoint);
    mRightMountainPath.lineTo(x + lrdPoint + getValue(10) + lrBanDis, y + lrmYpoint + lrBanGao);
    mRightMountainPath.lineTo(x + lrdPoint + getValue(10) - lrBanDis, y + lrmYpoint + lrBanGao);
    mRightMountainPath.close();

    // 中山
    mMidMountainPath.reset();
    mMidMountainPath.moveTo(x, y + down);
    mMidMountainPath.lineTo(x + getValue(220), y + down + mParentHeight / 2 + mParentHeight / 14);
    mMidMountainPath.lineTo(x - getValue(220), y + down + mParentHeight / 2 + mParentHeight / 14);
    mMidMountainPath.close();

    // 左右山移动的距离
    mMaxMouTranslationY = (y + down + mViewCircle) / 14;
}

然后我们在onDraw()里根据动态的y坐标去移动,以中间的山为例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 中间的山
mMidComputeMatrix.reset();
mMidComputePath.reset();
mMidComputeMatrix.postTranslate(0, mMaxMouTranslationY * mMidMouAnimatorValue);
mMidMountainPath.transform(mMidComputeMatrix, mMidComputePath);
canvas.drawPath(mMidComputePath, mMidMountainPaint);

mMidMouAnimatorValue变化,注意y坐标会先上升一点再下降:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 中间山的动画
 */
private void setMidMouAnimator(final boolean isFirst) {
    ValueAnimator mMidMouAnimator;
    if (isFirst) {
        mMidMouAnimator = ValueAnimator.ofFloat(0, -1, 10);
        mMidMouAnimator.setStartDelay(200);
        mMidMouAnimator.setDuration(1000);
    } else {
        mMidMouAnimator = ValueAnimator.ofFloat(10, 0);
        mMidMouAnimator.setStartDelay(0);
        mMidMouAnimator.setDuration(600);
    }
    mMidMouAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
    mMidMouAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mMidMouAnimatorValue = (float) animation.getAnimatedValue();
            invalidate();
        }
    });
    mMidMouAnimator.start();
}

4.云朵和左右平移动画

这次的动画和山的动画非常相似,只是由y坐标的变化改成x坐标的变化,但是绘制云朵稍微有点麻烦: 想要深入了解的可看这里:Android 自定义View之下雨动画 - 画云

总的来说是由四块view组成,底部的矩形(因为整体下移了所以这里基本没有看到矩形),还有矩形上面的三个圆形。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 绘制圆角矩形
path.addRoundRect(RectF rect, float rx, float ry, Direction dir)
// 绘制圆形
path.addCircle(float x, float y, float radius, Direction dir)

然后得到x坐标后根据增量值mCloudAnimatorValue进行动态移动:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mCloudComputeMatrix.postTranslate(mMaxCloudTranslationX * mCloudAnimatorValue, 0);
mCloudPath.transform(mCloudComputeMatrix, mCloudComputePath);
canvas.drawPath(mCloudComputePath, mCloudPaint);

然后我们将太阳的旋转动画、三座山的上下平移动画、云朵的左右平移动画,这五个动画组合起来就得到了一个完整的连贯动画。

最后

为了扩展性,我们给View增加一些属性,用来自定义颜色:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<declare-styleable name="SceneryView">
    <!--The color of sun-->
    <attr name="sun_color" format="color" />
    <!--The color of the cloud-->
    <attr name="cloud_color" format="color" />
    <!--The color of the left mountain-->
    <attr name="left_mountain_color" format="color" />
    <!--The color of the right mountain-->
    <attr name="right_mountain_color" format="color" />
    <!--The color of the middle mountain-->
    <attr name="mid_mountain_color" format="color" />
    <!--The color of the background-->
    <attr name="background_color" format="color" />
</declare-styleable>

这里的主要难点是动画的理解和使用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
matrix.postTranslate(dx, dy);
path.transform(matrix, momputePath);
canvas.drawPath(momputePath, mPaint);

我们通过动态改变dx和dy的值来达到动的效果,然后就是绘制三角形、圆形、圆角矩形以及它们坐标位置的动态处理。

以上源代码在这里可以拿到:SceneryView.java

参考资料

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021.11.21 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
自定义view实现超萌动感小炸弹
Hello,小伙伴们,我回来了。这些日子有的小伙伴问我怎么没有更新了。这个其实是有原因,首先,最近有点忙。其次没有看到什么觉得好玩的动画!最后,就是我更新过了!!ThreadLocal源码完全解析,只是你们源码不感冒,然后你们忽略了!!!!忽略了!!!还有,我其实有更新一个薄荷卷尺,只是觉得有点简单,而且还像也有什么好讲的,所以只是上传到github,没有文章。 客套话已经完了,现在开始我们的超萌动感小炸弹之旅。 首先,我还是先感谢一下作者,设计出这么棒的动画!!设计出处点我。 效果如下,Amazing:
用户2802329
2018/08/07
7510
自定义view实现超萌动感小炸弹
自定义View之雷达图
当MeasureSpec.getMode不等于 MeasureSpec.EXACTLY时即父控件没有指定大小时,该View的宽时这个屏幕的宽再加它的左右间隔,而高是该屏幕的高再加上下间隔再除于2,否侧就是用指定控件的大小。
HelloJack
2018/08/28
7820
自定义View之雷达图
自定义View学习——粗略实现商品图片内容描述
前段时间从朋友那听说一个需求,实现一个商品展示并且添加商品的描述,闲暇时间试着自己实现了一下。 下面看一下效果图:
木溪bo
2018/12/27
6810
你也可以自己写一个可爱 & 小资风格的Android加载等待自定义View
对比市面上的加载等待自定义控件,该控件Kawaii_LoadingView 的特点是:
Carson.Ho
2019/02/22
5660
Android自定义View:手把手带你实现一个小众、优雅的加载等待控件
对比市面上的加载等待自定义控件,该控件Kawaii_LoadingView 的特点是:
Carson.Ho
2022/03/25
8830
Android自定义View:手把手带你实现一个小众、优雅的加载等待控件
带你玩转自定义view系列
View是Android所有控件的基类,接下来借鉴网上的一张图片让大家一目了然(图片出自:http://blog.51cto.com/wangzhaoli/1292313)
Android技术干货分享
2019/03/26
1.7K0
带你玩转自定义view系列
感受一波Android自定义view实现超萌动感小炸弹!!
下面我们和自定义view实现超萌动感天气小太阳一样,开始解析动画!(没看过天气小太阳的朋友可以先去看天气小太阳,有些天气小太阳讲过的套路将不再讲,同时需要掌握path、camera、贝塞尔曲线等,不然部分代码可能会引起不适)。
用户9239674
2021/12/12
5250
Android自定义View【实战教程】4⃣️----BitmapShader详解及圆形、圆角、多边形实现
官方定义:Shader used to draw a bitmap as a texture BitmapShader的作用是使用特定的图片来作为纹理来使用。
先知先觉
2019/01/21
1.7K0
自定义View入门实战案例详析 | 蜘蛛网DataShowView
在onSizeChanged()中, 根据View的长宽, 获取整个布局的中心坐标, 以及计算网状多边形的半径, 后续整个蜘蛛网都是从这个中心坐标开始绘制的:
凌川江雪
2019/08/03
6280
Android查缺补漏(View篇)--自定义View利器Canvas和Paint详解
上篇文章介绍了自定义View的创建流程,从宏观上给出了一个自定义View的创建步骤,本篇是上一篇文章的延续,介绍了自定义View中两个必不可少的工具Canvas和Paint,从细节上更进一步的讲解自定义View的详细绘制方法。如果把自定义View比作盖一座房子,那么上篇文章就相当于教会了我们怎么一步步的搭建房子的骨架,而本篇文章将要教会我们的是为房子的骨架添砖加瓦直至成型,甚至是怎么装修。 Canvas 为了后文更为方便的讲解Canvas的常用方法的使用,我们先来做一些准备工作,创建一个自定义View框架,
codingblock
2018/03/30
1.2K0
Android--加载中动画View
效果如下: /** * 加载动画 */ public class SplashView extends View { //小球颜色 private int[] colors;
aruba
2020/07/03
1.1K0
Android--加载中动画View
Path类的最全面详解 - 自定义View应用系列
举例说明2:(非零环绕数规则) 从上面方法分析到,任何图形都是由点连成线组成的,是具备方向的,看下图:(矩形是顺时针)
Carson.Ho
2019/02/22
6860
Android自定义View之仿QQ未读消息拖拽效果
绘制以上两条贝塞尔曲线和直线需要五个点:P1,P2,P3,P4,M,其中P1,P2,P3,P4是圆的切点,现在只知道两个圆的中心圆点O1和O2,那么怎么根据这两个点来求其余四个圆的切点呢?先分析:
Rouse
2019/07/17
1.9K1
Android自定义View之仿QQ未读消息拖拽效果
Android自定义系列——10.PathMeasure
顾名思义,PathMeasure是一个用来测量Path的类,主要有以下方法: 构造方法
老马的编程之旅
2022/06/22
4070
Android自定义系列——10.PathMeasure
自定义View学习——仿QQ消息气泡拖拽黏连删除
该篇主要是对MessageBubbleView仿QQ消息控件的修改。因为我发现这个QQ消息气泡开源控件是规则的圆,所以稍加修改,对onDraw()绘画图形做了变动,更加接近于QQ气泡了。毕竟前人栽树后人乘凉,该控件又是通过手指触摸调用事件分发处理又是贝塞尔曲线的应用,多少目前能力有限,只有借鉴了。需要的文件图片请从文中提供的MessageBubbleView仿QQ消息控件下载。 参考博客:仿 QQ 未读消息气泡,可拖拽删除,粘连效果
木溪bo
2018/12/27
1.5K0
Android自定义控件:一款多特效的智能loadingView
画图首先是onDraw方法(我会把圆代码写上,一步一步剖析): 首先在view中定义个属性:private RectF rectf = new RectF();//可以理解为,装载控件按钮的区域
Android技术干货分享
2019/07/22
6340
推荐阅读
相关推荐
自定义view实现超萌动感小炸弹
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验