作者 | 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中界说的标签来完结自界说形状和相关作用,能够经过的相关属性来制造出各种形状,并为其使用突变色、阴影、边框等作用。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优化实践(三) 资源优化