Android UI 篇- 手势月亮 亮度动画

Android UI 篇- 手势月亮动画

一、应用场景

二、流程分析

三、代码实现(文章末尾彩蛋)

一、应用场景

1.1、先上效果图
2020-05-03 01_00_28.gif

一个有创意的亮度动画,通过手势上下滑动控制手机屏幕亮度,动画从太阳(天亮了)变成月亮(夜黑了),非常漂亮屏幕亮度动画!

二、流程分析

2.1、先上渐变图分析:
渐变图1
  • 首先需要画一个圆
  • 需要画出 A,B 两点之间的向圆心弯曲的弧度,和 A,B 本身的圆弧,然后用 PorterDuff.Mode.DST_OUT,去掉和圆重叠的部分,可以得出月亮。那么向圆心弯曲弧度需要用贝塞尔曲线画出,把 A,B 两点作为贝塞尔曲线的定点,并取A,B两点形成的线段的中垂线上的点C 作为控制点,画出月亮的缺角的弧度,如草图下 :
    1588491589781.jpg

变量之间的关系:OC ⊥ ABA,B两点是贝塞尔曲线定点,C点是贝塞尔曲线的控制点。OC = K * ABK 是一个经验值目前可以设置为0.43f(那么C点可以被解出来),这样的塞尔曲线的弧度比较美观,拟合圆的弧度。

  • 问题分解为要求出 A,B两点的位置,和 C点的位置,A,B两点一直在变化,A点跑得比较快,B点跑得比较慢,需要模拟出两点的位置(在progress ∈[0,1]的条件下的位置)。A,B两点的运动轨迹范围如草图下:
1588493267013.jpg

A,B 两点同时出发,A->A',B->B'.(逆时针)

  • A,B 两点可使用 PathMeasure.getPosTan(float distance, float pos[], float tan[]) 函数接口得出两点的位置,distance 代表的是 A 或者 B 点距离起点的距离(这个距离可以通过progress算出),pos[] 代表传入一个非空数组,函数执行完毕后,A或者B 点的坐标会复赋值到 pos[]中。tan[] 代表 A或者B 点的当前的导数,也就当前点切线的斜率(目前本动画无需用上)。分析完毕,代码撸起来。

备注:另外一种思路实现通过两个圆去 DST_OUT,得出第一个圆剩余的部分也是月亮,但是第二个圆的圆心轨迹函数,和半径变化轨迹函数,想要画出漂亮的月亮,两个运动轨迹没有规律可言,也很难被求出,只能适用于画静态的月亮,画不了动态的月亮。

三、代码实现

3.1、画圆形
  • 通过复写 onSizeChanged ,画出圆。
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 太阳/月亮 到光晕的间隔是两倍光晕的宽度
        int margin = mHaloHeight + mHaloWidth * 2;
*         //实际上太阳/月亮 具体宽高
        mLayer.set(margin, margin, w - margin, h - margin); 
        mCirclePath.reset();
        //取宽高中 最短的最为太阳的半径
        circleR = w > h ? (h - 2 * margin) / 2.0f : (w - 2 * margin) / 2.0f;
        //顺时钟画圆,圆的起始位置在右侧中间
        mCirclePath.addCircle(mLayer.centerX(), mLayer.centerY(), circleR, Path.Direction.CW);
        //把画好圆的 path 添加到 PathMeasure,待会可以被 getPosTan 使用
        mMeasure.setPath(mCirclePath, false); 
    }
3.2、求通过progressA、B定点
  • 这里画个图先讲解一下 getPosTan 的用法,因为下面开始要求 A,B 定点坐标。如下图,圆的起始位置在右侧中间,通过 getPosTan 接口 传入 0.25 * len,可以得到下图中 B 点的位置,len = mMeasure.getLength();

    1588496794943.jpg

  • A,B定点如何求得,我们要做到通过输入 progress ,求出 distance ,然后通过 distance 求出定点。(distance 就是A/B点距离原点的距离)

  • 首先我们通过上面getPosTan 的用法,知道如何求指定点的坐标,那么A,B两点可以通过progress求出,我们通过下面草图规定一下名词称呼:

    1588498848037.jpg

  • 先看 A 点运动轨迹,progress 进度 0->1 变化时。A->A' (第四象限->第一象限),也就是位置从 0.1 -> 0 (01 重叠),1 -> 0.9. 嗯。。如何用公式把这个映射给表达出来,输入 progress ,输出 distance 位置 。只能通过分段函数去表示,progress 0->0.5,输出distance 0.1 ->0progress 0.5->1 ,输出distance 1->0.9. 见下图,通过两点公式可以求得:

    1588500041091.jpg

    /**
     * 分段函数
     *
     * @param progress
     * @param point
     */
    private void getBeginPoint(float progress, float[] point) {
        if (progress <= 0.5) {
            //A 定点 在0.5 progress 之前都是在第四
            mMeasure.getPosTan(mMeasure.getLength() * (-0.2f * progress + 0.1f), point, null);
        } else {
            mMeasure.getPosTan(mMeasure.getLength() * (-0.2f * progress + 1.1f), point, null);
        }
    }
  • 依样画葫芦,我们来就 B 点运动轨迹的坐标,progress 进度 0->1变化时。B->B' (第四象限->第三象限),也就是位置从 0.1 -> 0 (01 重叠),1 -> 0.3. 通过分段函数去表示,progress 0->0.1B点速度较快),输出distance 0.1 ->0;当 progress 0.1->1 ,输出distance 1->0.3. 差不多这样子,通过两点公式可以求得:
    1588591051538.jpg

    /**
     * 分段函数
     *
     * @param progress
     * @param point
     */
    private void getSecondPoint(float progress, float[] point) {
        if (progress <= 0.1) {
            mMeasure.getPosTan(mMeasure.getLength() * (-1.0f * progress + 0.1f), point, null);
        } else {
            mMeasure.getPosTan(mMeasure.getLength() * (-7.0f / 9.0f * progress + 9.7f / 9f), point, null);

        }
    }
3.2、求控制点 C
1588606424855.jpg
  • 根据上图分析,控制点C 是AB 线段的中垂线上的一点;
  • CO = 0.43 *ABO为垂足);
  • O点横坐标,减去OD 就是C点横坐标 OD = cos α * OC
  • OC = 0.43 *AB
  • cosα = 1 / Math.sqrt(1 + tanα * tanα)(三角函数);
  • tanα = 中垂线 l 的斜率 K
  • 定理:l1垂直l2,那么k1*k2 = -1;那么中垂线的斜率可以通过AB 斜率得出;
  • 所有的获取变量搞定,上面只分析了B在第四象限,A在第一象限的情况。(还有B 在 四,A在 四;B 在四,A在二;B在四,A在三的情况)但是基本思路差不多(实际上是不想画了,号称灵魂画手的我,都画得快扛不住 ahhhhh。。。);
  • 直接上代码:

    private float[] getContrlPoint(float[] point1, float[] point2) {
        float centerX = mLayer.centerX();
        float centerY = mLayer.centerY();
        float diffDis = (float) Math.sqrt((point1[0] - point2[0]) * (point1[0] - point2[0]) + (point1[1] - point2[1]) * (point1[1] - point2[1]));
        //中垂线函数 y = kx+b 中的 k
        float k = (point1[0] - point2[0]) / (point2[1] - point1[1]);
        //中垂线函数 y = kx+b 中的 b
        float b = (point1[1] + point2[1]) / 2.0f - (point1[0] * point1[0] - point2[0] * point2[0]) / 2.0f / (point2[1] - point1[1]);
        float[] point = {0f, 0f};
        // cosα 的值
        float cosDegrees = (float) (1 / Math.sqrt(1 + k * k));
        if (k < 0) {
            //magicNum 为0.43
            point[0] = (point1[0] + point2[0]) / 2.0f - (cosDegrees * diffDis * magicNum); 
        } else if (k > 0) {
            if (point1[0] > centerX && point1[1] > centerY && point2[0] > centerX) {
                point[0] = (point1[0] + point2[0]) / 2.0f - (cosDegrees * diffDis * magicNum);
            } else {
                point[0] = (point1[0] + point2[0]) / 2.0f + (cosDegrees * diffDis * magicNum);
            }
        } else {
            point[0] = (point1[0] + point2[0]) / 2.0f;
        }

        point[1] = k * point[0] + b;
        return point;
    }
3.3、画 AB 向圆心弯曲的贝塞尔曲线
  • 这个就很简单了,两个定点和一个控制点求出来了,就直接画。
        //找到第一个定点
        getBeginPoint(progress, mBeginPoint);
        //找到第二个定点
        getSecondPoint(progress, mSecondPoint);


        mQuadPath.reset();
        mQuadPath.moveTo(mBeginPoint[0], mBeginPoint[1]);
        float[] begin = {mBeginPoint[0], mBeginPoint[1]};
        //找到拟合圆的贝赛尔曲线控制点
        float[] contrlPoint = getContrlPoint(begin, mSecondPoint);
        //画贝赛尔曲线
        mQuadPath.quadTo(contrlPoint[0], contrlPoint[1], mSecondPoint[0], mSecondPoint[1]);
3.4、画 AB 本身的圆弧
  • 画圆弧函数 public void arcTo(RectF oval, float startAngle, float sweepAngle);现在我们知道 AB两点坐标,能否求出 startAnglesweepAngle ,答案肯定是可以的。还是先上草图:
1588606677198.jpg
  • 上图给出的条件是 A 点在第一象限,B点在第三象限,圆弧的起始位置如草图所示,在圆的右侧中间,即startAngle。那么很明显AB本身圆弧(上方的圆弧)
  • startAngle=180°-∠BOH
  • sweepAngle=∠BOH+180°-∠AOH'
  • ∠BOH = Math.toDegrees(Math.asin(BH / circleR))
  • BH = B 点 Y 坐标 - centerY(圆心 Y 坐标)
  • ∠AOH'= Math.toDegrees(Math.asin(AH'/ circleR))
  • AH'= centerY(圆心 Y 坐标)- A 点 Y 坐标
  • 所有的获取变量搞定,上面只分析了A 点在第一象限,B点在第三象限的情况。(还有B 在 四,A在 四;B 在四,A在二;B在四,A在三的情况),基本套路一样
  • 下面直接给出代码:
 private Pair<Float, Float> getAngle(float[] point1, float[] point2) {
        float centerX = mLayer.centerX();
        float centerY = mLayer.centerY();
        float diffY;
        float degrees1 = 0;
        float degrees2;

        float startAngle;
        float sweepAngle;
        if (point2[0] > centerX && point2[1] > centerY) {
            degrees1 = (float) Math.toDegrees(Math.asin((point2[1] - centerY) / circleR));
            degrees2 = (float) Math.toDegrees(Math.asin((point1[1] - centerY) / circleR));
            startAngle = degrees1;
            sweepAngle = degrees2 - degrees1;
        } else {
            if (point2[0] > centerX) { //一 象限
                if (point2[1] < centerY) {
                    diffY = centerY - point2[1];
                    degrees1 = (float) Math.toDegrees(Math.asin(diffY / circleR));
                }
            } else { // 2 3 象限
                if (point2[1] < centerY) {
                    diffY = centerY - point2[1];
                    degrees1 = 180 - (float) Math.toDegrees(Math.asin(diffY / circleR));
                } else {
                    diffY = point2[1] - centerY;
                    degrees1 = (float) Math.toDegrees(Math.asin(diffY / circleR)) + 180;
                }
            }
            degrees2 = (float) Math.toDegrees(Math.asin((centerY - point1[1]) / circleR));

            startAngle = 360 - degrees1;
            sweepAngle = degrees1 - degrees2;
        }

        return new Pair<>(startAngle, sweepAngle);
    }
3.5、画光晕
  • 最后是画光晕,这个就比较简单,直接上代码
        //画光晕
        canvas.save();
        //画布平移到中间,为了等下旋转使用
        canvas.translate(mLayer.centerX(), mLayer.centerY());
        //计算出当前进度需要画多少个光晕
        int count = mNumOfHalo - (int) (progress / mOneOFHaleProgress);
        float mHalfHaloWidth = mHaloWidth / 2;
        //开始画光晕
        for (int i = 0; i < count; i++) {
            canvas.drawRoundRect(new RectF(-mHalfHaloWidth,-mLayer.centerY(),mHalfHaloWidth,mHaloHeight - mLayer.centerY()), mHalfHaloWidth, mHalfHaloWidth, mPaint);
            canvas.rotate(mOneOFHaleDegrees);
        }
        canvas.restore();

彩蛋:到这基本就大功告成,虽然动画的细节非常多,但是我们还是把它给画出来了。给出了亮度的动画,这不还得要一个音量调节的动画?左边上下滑动调节亮度,右边上下滑动调节音量。(号称视频播放双动画)好的,这就给你献上,直接拿走不谢!

2020-05-05 18_09_07.gif

下面给出这个两个动画的开源代码,以及使用教程。github传送门。记得给个 Star (点个赞) 哦。

推荐阅读:Android 编译速度优化黑科技 - RocketX

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335