Android实现Dribbble上动感的Gallery App Icon

news2024/11/23 20:18:56

先来看看原Dribbble上动感的Gallery App Icon效果图

思路

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

  • 圆形背景

  • 太阳(圆形)

  • 山(三角形)

  • 云朵(圆角矩形 + 三个圆)

需要进行的动画:

  • 太阳 - 旋转动画

  • 山 - 上下平移动画

  • 云朵 - 左右平移动画

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

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

绘制

1、圆形背景

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

    @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获取:

    @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背景色设置一个颜色就好:

mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBackgroundPaint.setColor(mBackgroundColor);

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

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

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

canvas.drawCircle((mParentWidth / 2) - getValue(90), (mParentHeight / 2) - getValue(80), sunWidth / 2, mSunPaint);

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

/**
 * 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来设置动画值就可以了。

/**
 * 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坐标:

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是将角度转换为弧度。

/**
 * 求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坐标达到旋转的效果:

// 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]。这样就可以执行太阳的旋转动画:

/**
 * 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));
/**
 * 画中间的三座山
 *
 * @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坐标去移动,以中间的山为例:

// 中间的山
mMidComputeMatrix.reset();
mMidComputePath.reset();
mMidComputeMatrix.postTranslate(0, mMaxMouTranslationY * mMidMouAnimatorValue);
mMidMountainPath.transform(mMidComputeMatrix, mMidComputePath);
canvas.drawPath(mMidComputePath, mMidMountainPaint);

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

/**
 * 中间山的动画
 */
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坐标的变化,但是绘制云朵稍微有点麻烦,总的来说是由四块view组成,底部的矩形(因为整体下移了所以这里基本没有看到矩形),还有矩形上面的三个圆形。

// 绘制圆角矩形
path.addRoundRect(RectF rect, float rx, float ry, Direction dir)
// 绘制圆形
path.addCircle(float x, float y, float radius, Direction dir)

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

mCloudComputeMatrix.postTranslate(mMaxCloudTranslationX * mCloudAnimatorValue, 0);
mCloudPath.transform(mCloudComputeMatrix, mCloudComputePath);
canvas.drawPath(mCloudComputePath, mCloudPaint);

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

最后

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

<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>

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

matrix.postTranslate(dx, dy);
path.transform(matrix, momputePath);
canvas.drawPath(momputePath, mPaint);

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

源码地址:

https://github.com/youlookwhat/SceneryView/blob/master/library/src/main/java/me/jingbin/scenery/SceneryView.java

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/365862.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

随想录二刷 (双指针法) leetcode 27 26 283 844

双指针法的原理 双指针法相对于暴力解法的优点有以下几点 暴力遍历的时间复杂度会比较高双指针法利用两个指针进行遍历完成双层循环所做的事情 双指针一般有两种方法 同向指针&#xff0c;双向指针 第一题 leetcode 27 移除元素 题目描述 题目分析 采用暴力遍历可以得出结…

vector的基本使用

目录 介绍&#xff1a; vector iterator 的使用 增删查改 增&#xff08;push_back insert&#xff09;&#xff1a; 删(pop_back erase)&#xff1a; swap&#xff1a; vector的容量和扩容&#xff1a; 排序&#xff08;sort&#xff09;&#xff1a; 介绍&#xff…

SpringBoot入门(二)

这里写目录标题一、SpringBoot整合Junit1.1 搭建SpringBoot工程1.2 引入starter-test起步依赖1.3 编写类1.4 测试二、SpringBoot整合mybatis2.1 搭建SpringBoot工程2.2 引入mybatis起步依赖&#xff0c;添加驱动2.3 编写DataSource和MyBatis相关配置2.4 定义表和实体类2.5 编写…

100%BIM学员的疑惑:不会CAD可以学Revit吗?

在新一轮科技创新和产业变革中&#xff0c;信息化与建筑业的融合发展已成为建筑业发展的方向&#xff0c;将对建筑业发展带来战略性和全局性的影响。 建筑业是传统产业&#xff0c;推动建筑业科技创新&#xff0c;加快推进信息化发展&#xff0c;激发创新活力&#xff0c;培育…

apk中代码执行adb指令实现

背景&#xff1a;想要在android apk中直接使用adb指令&#xff0c;从而不需要把手机通过数据线方式连接到电脑&#xff0c;在电脑端执行adb指令。 一、权限相关 想要在apk代码中执行adb命令&#xff0c;涉及到执行权限。 首先手机需要有root权限。其次就算手机已经root了&…

yolov5/6/7系列模型训练日志结果数据对比分析可视化

早在之前使用yolov3和yolov4这类项目的时候可视化分析大都是自己去做的&#xff0c;到了yolov5的时候&#xff0c;变成了一个工具包了&#xff0c;作者全部集成进去了&#xff0c;这里我们以一个具体的结果为例&#xff0c;如下&#xff1a;整个训练过程产生的指标等数据都会自…

11.3 基于Django4的可重用、用户注册和登录系统搭建(优化)

文章目录邮件注册发送邮件功能测试基本的邮件注册功能实现完成注册表单完成注册的业务逻辑密码加密功能实现邮件注册确认创建模型修改视图测试处理邮件确认请求修改登录规则测试邮件注册 根据官方文档进行&#xff1a;https://docs.djangoproject.com/zh-hans/4.1/topics/emai…

什么是智慧实验室?

智慧实验室是利用现代信息技术和先进设备将实验室实现智能化和智慧化的概念。通过将各种数据、信息和资源整合在一起&#xff0c;实现实验室设备的互联互通&#xff0c;数据的实时采集、传输、处理和分析&#xff0c;从而提高实验室的效率、精度和可靠性。一、智慧实验室包含多…

Java~对于代码块与内部类的理解

目录 代码块 普通代码块 构造代码块 静态代码块 内部类 成员内部类 普通内部类 静态内部类 局部内部类 代码块 使用“{}”定义的一段代码成为代码块&#xff0c;代码块分为普通代码块、构造代码块、匿名代码块、同步代码块。 普通代码块 定义在方法中的代码&#x…

【go语言之thrift协议一】

go语言之thrift协议thrift文件shared.thriftSharedStructSharedServiceSharedServiceProcessorSharedServiceGetStructArgsSharedServiceGetStructResulttutorial.thrift基本数据类型引入其他thrift文件自定义类型定义常量enum继承thrift 相对于grpc而言&#xff0c;可能用的不…

逆向-还原代码之max 再画堆栈图 (Interl 64)

// source code #include <stdio.h> void max(int * a, int * b) { if (*a < *b) *a *b; } int main() { int a 5, b 6; max(&a, &b); printf("a, b max %d\n", a); return 0; } // 再画堆栈图 下周一&#xff08;2.27…

JavaEE简单示例——MyBatis关联映射

简单介绍&#xff1a; 在我们之前的案例中&#xff0c;我们进行了简单查询&#xff0c;条件产村&#xff0c;动态SQL的条件查询&#xff0c;但是这些操作都是在一张表中进行的&#xff0c;而在我们之前学习MySQL中还有一个很重要的操作就是多表查询操作&#xff0c;也就是说通…

图像亮度调整

非线性方式 调整图像的方法有很多&#xff0c;最常用的方法就是对图像像素点的R、G、B三个分量同时进行增加&#xff08;减少&#xff09;某个值&#xff0c;达到调整亮度的目的。即改变图像的亮度&#xff0c;实际就是对像素点的各颜色分量值做一个平移。这种方法属于非线性的…

适用于产研团队协作工具有哪些?盘点6大类协同办公软件

团队协作工具在提高团队协作效率、质量和灵活性&#xff0c;降低成本等方面都有着不小的作用。而根据协作内容、团队等特点的不同&#xff0c;团队协作工具可以分为多种类型&#xff0c;常见的包括&#xff1a;即时通讯工具&#xff0c;用于实时交流和沟通&#xff0c;其中又可…

SpringBoot整合JPA+人大金仓(kingbase8)

陈老老老板&#x1f9b8;&#x1f468;‍&#x1f4bb;本文专栏&#xff1a;国产数据库-人大金仓&#xff08;kingbase8&#xff09;&#xff08;主要讲一些人大金仓数据库相关的内容&#xff09;&#x1f468;‍&#x1f4bb;本文简述&#xff1a;本文讲一下Jpa框架整合人大金…

Spring Cloud Nacos源码讲解(三)- Nacos客户端实例注册源码分析

Nacos客户端实例注册源码分析 实例客户端注册入口 流程图&#xff1a; 实际上我们在真实的生产环境中&#xff0c;我们要让某一个服务注册到Nacos中&#xff0c;我们首先要引入一个依赖&#xff1a; <dependency><groupId>com.alibaba.cloud</groupId><…

Bootstrap入门到精通(最全最详细)

文章目录前言一、Bootstrap是什么&#xff1f;二、Bootstrap安装方式一&#xff1a;将压缩包下载到本地引入使用方式二&#xff1a;使用Bootstrap官方cdn二.Bootstrap容器下面是屏幕宽度在不同大小时不同容器的显示状态三.Bootstrap栅格系统bootstrap网格系统有以下六个类网格系…

上岸!选择你的隐私计算导师!

开放隐私计算 开放隐私计算开放隐私计算OpenMPC是国内第一个且影响力最大的隐私计算开放社区。社区秉承开放共享的精神&#xff0c;专注于隐私计算行业的研究与布道。社区致力于隐私计算技术的传播&#xff0c;愿成为中国 “隐私计算最后一公里的服务区”。183篇原创内容公众号…

剑指 Offer 55 - I. 二叉树的深度

摘要 剑指 Offer 55 - I. 二叉树的深度 一、深度优先搜索 如果我们知道了左子树和右子树的最大深度l和r&#xff0c;那么该二叉树的最大深度即为&#xff1a;max(l,r)1。 而左子树和右子树的最大深度又可以以同样的方式进行计算。因此我们可以用「深度优先搜索」的方法来计…

JTT808jt1078

List item 前言 交通部与2016年10月份推出了JT/T 1078-2016标准&#xff0c;全称是<道路运输车辆卫星定位系统视频通信协议> 实时音视频传输指令 实时音视频传输请求 消息 I&#xff24;&#xff1a;&#xff10;x9101。 报文类型&#xff1a;信令数据报文。 平台向终…