写点什么

扫光动效在移动端应用实践

作者:百度Geek说
  • 2023-07-04
    上海
  • 本文字数:7665 字

    阅读完需:约 25 分钟

扫光动效在移动端应用实践

作者 | 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 = 定时器间隔时间intervalTimeself.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 方法 中依次绘制源位图和目标位图:


// 先绘制一个未经过遮罩处理的位图,作为 srcdrawUnmasked(new Canvas(unmaskBitmap));
Canvas unmaskRenderCanvas = (new Canvas(maskBitmap));unmaskRenderCanvas.drawColor(0, PorterDuff.Mode.CLEAR);super.dispatchDraw(unmaskRenderCanvas);canvas.drawBitmap(unmaskBitmap, 0, 0, mAlphaPaint);
// 再绘制一个经过遮罩处理的位图,作为dstCanvas 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() {@Overridepublic 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优化实践(三) 资源优化

发布于: 刚刚阅读数: 3
用户头像

百度Geek说

关注

百度官方技术账号 2021-01-22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
扫光动效在移动端应用实践_动效_百度Geek说_InfoQ写作社区