作者 | Seven
导读
随着挪动互联网的疾速倒退,业界涌现出大量有创意又乏味的交互体验。扫光动效就是其中一种有意思的加载动效,常见的扫光动效有骨架屏扫光、logo 扫光。那么这两种扫光动效的原理是什么,如何实现这两种扫光成果,以及在 iOS 和 Andoird 双端实现起来有什么差别,本文会为你具体揭晓。
全文 10549 字,预计浏览工夫 27 分钟。
01 引言
扫光动效作为挪动端的常见加载动效,与传统的转圈加载相比,能给人更好的视觉和感官体验。其次要特点是光效会随着工夫进行扫射,文字或图案有被色彩填充的感觉。
笔者先后做过骨架屏扫光、熊掌扫光 loading, 本文将别离从 iOS 和 Android 的视角, 介绍这两种扫光动效的实现和双端的技术差别。
△熊掌扫光动效
02 骨架屏扫光动效
骨架屏是一种界面加载过程中的过渡成果。它在页面数据加载实现前,先给用户展现出页面的大抵构造,在拿到接口数据后渲染出理论页面内容而后替换掉。这种技术可能升高用户的焦灼情绪,使界面加载过程变得天然通顺,晋升用户体验。罕用于文章列表、动静列表页等绝对比拟规定的列表页面。这里以领取半屏面板面板为例,能够看到有光效在骨架图上扫过的成果。
△骨架屏扫光
2.1 骨架屏扫光原理剖析
骨架屏的扫光场景比较简单, 因为其背景是不通明, 能够通过在骨架图下面叠加一个遮罩视图作为光块,对遮罩进行挪动来达到扫光的成果。
其视图的层级整体分位两层,底层为自定义视图的骨架局部,下层为突变通明遮罩。其中,骨架图局部为惯例的列表实现,这里不再赘述,而突变通明遮罩作为扫光的,能够用切图或通过代码来实现。遮罩切图绝对代码实现,会减少一部分包体积,所以能够抉择自定义一个和骨架图一样大小的视图,笼罩到骨架图之上,并设置其为从左到右突变通明。
此外,位移动画,能够通过设置在 xxx 工夫内,将遮罩视图在程度方向上,从骨架图的左侧挪动到骨架图的右侧来实现。
2.2 iOS 实现
Core Animation 是 AppKit 和 UIKit 完满的底层反对,同时也被整合进入 Cocoa 和 Cocoa Touch 的工作流之中,它是 app 界面渲染和构建的最基础架构。Core Animation 主要职责蕴含:渲染、构建和实现动画,尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的 layer(iOS 中具体而言就是 CALayer),并且被存储为树状层级构造。这个树也造成了 UIKit 以及在 iOS 应用程序当中咱们所能在屏幕上看见的所有的根底。
在 iOS 上,能够通过 CALayer +View 动画办法 +Transform 来实现。layer 是 UIView 的底层图层, 负责视图的绘制,动画,边框,暗影等视觉效果。动画局部间接用 View 的类办法 animateWithDuration 即可,在动画回调中通过设置视图的 Transform 属性来实现程度位移。
突变遮罩局部能够按如下代码定义,通过一个 ImageView 作为遮罩视图,通过设置 CAGradientLayer 做为其 layer 实现通明突变成果:
// 创立自定义视图作为遮罩视图
_lightCover = [[UIImageView alloc] initWithFrame:self.bounds];
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = _lightCover.bounds;
// 渐变色色彩数组
gradientLayer.colors = [NSArray arrayWithObjects:
(id)[UIColorFromRGBA(0xFFFFFF, 0) CGColor],
(id)[UIColorFromRGBA(0xFFFFFF, 0.3) CGColor],
(id)[UIColorFromRGBA(0xFFFFFF, 0.5) CGColor],
(id)[UIColorFromRGBA(0xFFFFFF, 0.3) CGColor],
(id)[UIColorFromRGBA(0xFFFFFF, 0) CGColor], nil];
// 突变的开始点 (不同的起始点能够实现不同地位的突变, 如图)
gradientLayer.startPoint = CGPointMake(0, 0.5f);
// 突变的完结点
gradientLayer.endPoint = CGPointMake(1, 0.5f);
// 把突变图层增加到遮罩视图的顶层
[_lightCover.layer insertSublayer:gradientLayer atIndex:0];
// 设置初始地位
_lightCover.transform = CGAffineTransformMakeTranslation(-self.bounds.size.width, 0);
通过定时器循环位移动画:
// 定时器:动画工夫 duration + 延迟时间 delay = 定时器间隔时间 intervalTime
self.lightSweepTimer = [NSTimer scheduledTimerWithTimeInterval:intervalTime target:self selector:@selector(lightSweepAnimation) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.lightSweepTimer forMode:NSRunLoopCommonModes];
动画局部间接用 View 的 animateWithDuration 办法实现:
self.lightCover.transform = CGAffineTransformMakeTranslation(-self.bounds.size.width, 0);
[UIView animateWithDuration:duration animations:^{self.lightCover.transform = CGAffineTransformMakeTranslation(self.bounds.size.width, 0.f);
} completion:^(BOOL finished) {}];
因为定时器执行工夫较长,能够在加载时先执行一次动画:
// 定时器工夫较长,先执行一次动画
[self lightSweepAnimation];
2.3 Android 实现
Android 的渲染技术次要建设在 View 零碎之上,View 零碎解决视图的布局和绘制。View 代表一个控件,次要负责本人的绘制,ViewGroup 代表一个容器,次要负责管理和布局它蕴含的子 View 和子 ViewGroup。
这里能够通过自定义 Shape+ObjectAnimator 实现。Shape 是一种非凡的 View,通过 XML 中定义的 <shape> 标签来实现自定义形态和相干成果,能够通过 <shape> 的相干属性来绘制出各种形态, 并为其利用渐变色、暗影、边框等成果。ObjectAnimator 能够用于所有反对动画的属性,包含地位、大小、旋转、缩放和透明度等,只需指定要动画的属性名称和目标值即可。
遮罩局部可按如下定义一个突变矩形:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="#00ffffff"
android:centerColor="#7fffffff"
android:endColor="#00ffffff"
></gradient>
</shape>
再定义一个 Handler,用于在主线程刷新视图:
Handler mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
int what = msg.what;
if (msgAnimation == what) {runAnimation();
}
return false;
}
});
通过 ObjectAnimator 以及属性 translationX 定义位移动画:
private void runAnimation() {if (displayWidth == 0) {displayWidth = defaultWidth;}
ObjectAnimator translationX = ObjectAnimator.ofFloat(mMoveLight, "translationX", -displayWidth, displayWidth);
translationX.setDuration(duration);
translationX.setRepeatCount(0);
translationX.start();
translationX.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {sendMsg(msgAnimation, delayTime);
}
});
}
提早一段时间后继续执行动画:
private void sendMsg(int what, int delayTime) {checkParent();
if (mHandler != null) {mHandler.sendEmptyMessageDelayed(what, delayTime);
}
}
开始加载的时候,先执行一次动画:
public void startLoading() {setVisibility(VISIBLE);
sendMsg(msgAnimation, 0);
}
03 熊掌扫光动效
熊掌扫光次要是作为页面加载时的过渡成果,会在内容加载实现前展现,其通常在页面内容下面,不能齐全遮挡底部内容。而且在日间模式(存在多种内容背景底色),夜间模式(在日间模式的根底上,笼罩了一层灰色通明蒙层),暗黑模式多种场景模式下,也会对扫光的成果产生烦扰,尤其是在日间模式灰色背景以及夜间模式下,甚至可能无奈看到扫光。
△别离为日间模式白底,日间模式灰底,夜间模式,暗黑模式
3.1 iOS 实现
熊掌扫光的简单场景,仅靠双层视图叠加无奈满足需要,滑块会有各种异常情况(具体见 3.2.1 局部)。在 iOS 上能够应用三层构造,最底层是待扫光的图,两头是挪动的光块,最上层是依据底图绘制的镂空图层,三层视图叠加在一起,造成光和图案混合的成果。
△iOS 通过遮罩实现扫光成果原理
iOS 的 CoreAnimation 框架十分优良,其 View 的实现,刚好满足了这种三层构造须要。
- view 视图
- View 是根本的用户界面元素,用于展现和解决用户界面。它们能够是规范的 UI 控件(如 UILabel、UIButton 等),也能够是自定义的视图。
- 每个 View 都有本人的绘制区域,能够蕴含其余视图作为其子视图。
- layer 图层
- Layer 是 View 的底层绘制层次结构中的一个组成部分。每个 View 都有一个与之关联的 Layer 对象(CALayer 类的实例)。
- Layer 负责解决 View 的内容的绘制和显示,包含视图的背景色彩、边框、暗影等。每个 Layer 都有一个本人的绘制区域,与 View 的边界对应。
- mask 遮罩
- Mask 是一种用于管制图层可见性的机制。它是一个通明的图像或形态,能够与 Layer 关联。
- 通过利用遮罩,能够定义 Layer 中哪些区域应该是可见的,哪些区域应该是暗藏的。
- 遮罩通常是由另一个 Layer 或者自定义的图像创立的,它们确定了图层中内容的可见局部。
iOS 能够通过 layer 作为光,mask 作为遮罩,来实现光混合在熊掌 logo 上的成果:
// loadingView 设置熊掌底图
self.loadingImgView.image = [self lightImg];
// 创立突变图层
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = CGRectMake(0, 0, loadingWidth, loadingHeight);
// 设置突变色彩
gradientLayer.colors = @[(__bridge id)[UIColor colorWithWhite:1 alpha:0].CGColor,
(__bridge id)[UIColor colorWithWhite:1 alpha:0.9].CGColor,
(__bridge id)[UIColor colorWithWhite:1 alpha:0.9].CGColor,
(__bridge id)[UIColor colorWithWhite:1 alpha:0].CGColor];
gradientLayer.locations = @[@0.0, @0.49, @0.495, @1.0];
gradientLayer.startPoint = CGPointMake(0, 0.5);
gradientLayer.endPoint = CGPointMake(1, 0.35);
[self.loadingImgView.layer addSublayer:gradientLayer];
// 创立通明遮罩
CALayer *maskLayer = [[CALayer alloc] init];
maskLayer.frame = CGRectMake(0, 0, loadingWidth, loadingHeight);
maskLayer.backgroundColor = [UIColor clearColor].CGColor;
// 设置遮罩内容
maskLayer.contents = (__bridge id _Nullable)([self lightImg].CGImage);
self.loadingImgView.layer.mask = maskLayer;
在 layer 层通过 CABasicAnimation 实现程度位移:
// 定义根本动画, 管制在 x 轴方向的位移
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
animation.duration = 2;
// 反复次数 1000000 次(有限次)
animation.repeatCount = 1000000;
// 动画不会主动反转
animation.autoreverses = false;
animation.fromValue = @(-loadingWidth);
animation.toValue = @(loadingWidth);
// 动画在实现后不会被移除
animation.removedOnCompletion = NO;
// 动画完结后图层放弃最初一个状态
animation.fillMode = kCAFillModeForwards;
[self.gradientLayer addAnimation:animation forKey:@"loading_animation_key"];
3.2 Android 实现
这里以 Andoird 上的双层视图叠加为例,能够看到会有各种各样的问题。
3.2.1 通过双层自定义视图叠加
通过叠加切图实现,存在的问题有:日间模式 (灰色背景) 下无奈看到滑块,同时暗黑模式下滑块较为显著。
△叠加切图
通过叠加 Shape 实现,在骨架屏场景上进行扫光成果还行,但在熊掌扫光上成果不佳。存在的问题有:如果是日间模式下的的白底背景失常,但灰底背景无奈看到扫润滑块。此外,不加旋转角度,在暗黑模式成果还行,叠加旋转度角度后,能够看到显著的滑块痕迹。
△叠加 shape 图层
△叠加 shape 图层(旋转角度)
甚至还能够通过 LinearGradient 实现自定义带斜率的突变扫光,其外围办法如下:
// float k = 1f * h / w;
mValueAnimator = ValueAnimator.ofFloat(0f - offset * 2, w + offset * 2);
mValueAnimator.setRepeatCount(repeatCount);
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.setDuration(duration);
mValueAnimator.addUpdateListener(animation -> {float value = (float) animation.getAnimatedValue();
LinearGradient mLinearGradient = new LinearGradient(
value,
k * value,
value + offset,
k * (value + offset),
colors,
positions,
Shader.TileMode.CLAMP
);
mPaint.setShader(mLinearGradient);
invalidate();});
mValueAnimator.start();
在 ValueAnimator 的更新回调中:
- 依据 value 和斜率 k 计算两个控制点的坐标
- 创立一个线性突变 LinearGradient, 由这两个控制点定义
- 将该突变设置为 Paint 的 Shader
- 调用 invalidate()重绘 View
因为 ValueAnimator 不断更新, 所以线性突变的两个控制点也在一直变动, 产生突变动画成果:
然而这种形式,在仅白底背景下成果较好,在灰底背景或暗黑成果下不尽人意,能够看到较显著的滑块。
3.2.2 通过 Canvas 绘图
Android 上不像 iOS 一样,View 自身还能够再设置多个图层。如果要混合渲染扫光和背景图,除非本人再自定义一个遮罩层造成三层构造,或者间接通过更底层的绘图来解决。
那么,怎么把扫光和背景图混合渲染呢?答案是能够通过 PorterDuffXferMode 来实现。
PorterDuffXferMode 应用 PorterDuff.Mode 规定将所绘制图形和 Canvas 上图形混合,最终更新 Canvas 展现新的图形。PorterDuffXferMode 的应用也非常简单,在须要应用的时候 paint.setXfermode(PorterDuff.Mode mode)设置混合模式。
PorterDuff.Mode 共分为 16 种模式:CLEAR、SRC、DST、SRC\_OVER、DST\_OVER、SRC\_IN、DST\_IN、SRC\_OUT、DST\_OUT、SRC\_ATOP、DST\_ATOP、XOR、DARKEN、LIGHTEN、MULTIPLY、SCREEN。
Android 应用 Canvas 在 View 上绘制图形,所绘制的图形中的像素称作源像素(source,简称 src),所绘制的矩形在 Canvas 中对应地位的矩形内的像素称作指标像素(destination,简称 dst)。源像素的 ARGB 四个重量会和 Canvas 上同一地位处的指标像素的 ARGB 四个重量依照 Xfermode 定义的规定进行计算,造成最终的 ARGB 值,而后用该最终的 ARGB 值更新指标像素的 ARGB 值。
以官网提供的图示来阐明,假如有一个蓝色的源像素图形和一个红色的指标像素图形。
通过 DST\_IN,能够失去相交局部是红色的扇形,即相交的局部保留指标像素,不相交的局部,抛弃源像素。
这样,首先通过 PorterDuffXfermode 来设置 DST\_IN 的混合成果,通过 LinearGradient 来创立遮罩的突变成果。其次应用 Canvas 和 Paint 来绘制和渲染位图,先绘制一个未通过遮罩解决的位图,作为 src,再绘制一个通过遮罩解决的位图,作为 dst,两者组合一起,造成扫光成果,最初在通过应用 ValueAnimator 来实现动画成果。
△图中亮光的局部为 dst
首先,创立带斜率的突变遮罩位图:
mMaskBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mMaskBitmap);
// 能够通过参数管制 getGradientColors 的值,在不同模式下为不同的突变色彩
Shader gradient = new LinearGradient(
0, 0,
width, 0,
getGradientColors(),
getGradientPositions(),
Shader.TileMode.REPEAT);
canvas.rotate(mTilt, width / 2, height / 2);
Paint paint = new Paint();
paint.setShader(gradient);
// 适度增大矩形区域,适配歪斜
int padding = (int) (Math.sqrt(2) * Math.max(width, height)) / 2;
canvas.drawRect(-padding, -padding, width + padding, height + padding, paint);
其次,在 dispatchDraw 办法 中顺次绘制源位图和指标位图:
// 先绘制一个未通过遮罩解决的位图,作为 src
drawUnmasked(new Canvas(unmaskBitmap));
Canvas unmaskRenderCanvas = (new Canvas(maskBitmap));
unmaskRenderCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
super.dispatchDraw(unmaskRenderCanvas);
canvas.drawBitmap(unmaskBitmap, 0, 0, mAlphaPaint);
// 再绘制一个通过遮罩解决的位图,作为 dst
Canvas maskRenderCanvas = (new Canvas(maskBitmap));
maskRenderCanvas.clipRect(
mMaskOffsetX,
mMaskOffsetY,
mMaskOffsetX + maskBitmap.getWidth(),
mMaskOffsetY + maskBitmap.getHeight());
maskRenderCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
super.dispatchDraw(maskRenderCanvas);
maskRenderCanvas.drawBitmap(maskBitmap, mMaskOffsetX, mMaskOffsetY, mMaskPaint);
canvas.drawBitmap(maskBitmap, 0, 0, null);
接着,通过 ValueAnimator 实现位移并触发实时绘制闪光成果:
mMaskTranslation.set(-width, 0, width, 0);
mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f + (float) mRepeatDelay / mDuration);
mAnimator.setDuration(mDuration + mRepeatDelay);
mAnimator.setRepeatCount(mRepeatCount);
mAnimator.setRepeatMode(mRepeatMode);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {float value = Math.max(0.0f, Math.min(1.0f, (Float) animation.getAnimatedValue()));
mMaskOffsetX = (int) (mMaskTranslation.fromX * (1 - value) + mMaskTranslation.toX * value);
mMaskOffsetY = (int) (mMaskTranslation.fromY * (1 - value) + mMaskTranslation.toY * value);
invalidate();}
});
最终成果如下图所示:
△日间模式
△夜间模式
△暗黑模式
04 结语
在下面内容中,咱们介绍到了基于遮罩实现的扫光成果,遮罩常见的利用有圆角成果,穿人像弹幕,还有在老手指引中用于绘制挖孔成果,或者是刮彩票成果。
在渲染技术上次要是使用到了 iOS 零碎中的 Core Animation 框架以及 Android 的 View 零碎。
iOS 上通常会应用 Core Animation 来高效、不便地实现动画。它应用 CALayer 进行图形渲染和动画操作。Apple 并没有间接在 UIView 上提供 masking 的反对,而是在其底层的 CALayer 上实现。这使开发者能够灵便管制和批改 mask, 达到更弱小的成果。而 Android 想要制作更灵便和弱小的成果,能够通过 Canvas 来实现。
——END——
举荐浏览:
Android SDK 平安加固问题与剖析
搜寻语义模型的大规模量化实际
如何设计一个高效的分布式日志服务平台
视频与图片检索中的多模态语义匹配模型:原理、启发、利用与瞻望
百度离线资源治理
百度 APP iOS 端包体积 50M 优化实际(三) 资源优化