Android UI 篇- 手势月亮动画
一、应用场景
1.1、先上效果图
一个有创意的亮度动画,通过手势上下滑动控制手机屏幕亮度,动画从太阳(天亮了)变成月亮(夜黑了),非常漂亮屏幕亮度动画!
二、流程分析
2.1、先上渐变图分析:
- 首先需要画一个圆
- 需要画出
A,B
两点之间的向圆心弯曲的弧度,和A,B
本身的圆弧,然后用PorterDuff.Mode.DST_OUT
,去掉和圆重叠的部分,可以得出月亮。那么向圆心弯曲弧度需要用贝塞尔曲线画出,把A,B
两点作为贝塞尔曲线的定点,并取A,B
两点形成的线段的中垂线上的点C
作为控制点,画出月亮的缺角的弧度,如草图下 :
变量之间的关系:
OC ⊥ AB
,A,B
两点是贝塞尔曲线定点,C
点是贝塞尔曲线的控制点。OC = K * AB
,K
是一个经验值目前可以设置为0.43f
(那么C
点可以被解出来),这样的塞尔曲线的弧度比较美观,拟合圆的弧度。
- 问题分解为要求出
A,B
两点的位置,和C
点的位置,A,B
两点一直在变化,A
点跑得比较快,B
点跑得比较慢,需要模拟出两点的位置(在progress ∈[0,1]
的条件下的位置)。A,B
两点的运动轨迹范围如草图下:
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、求通过progress
求A、B
定点
-
这里画个图先讲解一下
getPosTan
的用法,因为下面开始要求A,B
定点坐标。如下图,圆的起始位置在右侧中间,通过getPosTan
接口 传入0.25 * len
,可以得到下图中B
点的位置,len = mMeasure.getLength();
A,B
定点如何求得,我们要做到通过输入progress
,求出distance
,然后通过distance
求出定点。(distance
就是A/B
点距离原点的距离)-
首先我们通过上面
getPosTan
的用法,知道如何求指定点的坐标,那么A,B
两点可以通过progress
求出,我们通过下面草图规定一下名词称呼:
-
先看
A
点运动轨迹,progress
进度0->1
变化时。A->A'
(第四象限->第一象限),也就是位置从0.1 -> 0
(0
和1
重叠),1 -> 0.9
. 嗯。。如何用公式把这个映射给表达出来,输入progress
,输出distance
位置 。只能通过分段函数去表示,progress 0->0.5
,输出distance 0.1 ->0
;progress 0.5->1
,输出distance 1->0.9
. 见下图,通过两点公式可以求得:
/**
* 分段函数
*
* @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
(0
和1
重叠),1 -> 0.3
. 通过分段函数去表示,progress 0->0.1
(B
点速度较快),输出distance 0.1 ->0
;当progress 0.1->1
,输出distance 1->0.3
. 差不多这样子,通过两点公式可以求得:
/**
* 分段函数
*
* @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
- 根据上图分析,控制点C 是AB 线段的中垂线上的一点;
-
CO
=0.43 *AB
(O
为垂足); -
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两点坐标,能否求出startAngle
和sweepAngle
,答案肯定是可以的。还是先上草图:
- 上图给出的条件是
A
点在第一象限,B
点在第三象限,圆弧的起始位置如草图所示,在圆的右侧中间,即startAngle
为0°
。那么很明显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();
彩蛋:到这基本就大功告成,虽然动画的细节非常多,但是我们还是把它给画出来了。给出了亮度的动画,这不还得要一个音量调节的动画?左边上下滑动调节亮度,右边上下滑动调节音量。(号称视频播放双动画)好的,这就给你献上,直接拿走不谢!
下面给出这个两个动画的开源代码,以及使用教程。github传送门。记得给个 Star
(点个赞) 哦。