本文主要介绍如何在Android和iOS设备上,用同一套C语言代码实现一组动画效果的设计和开发过程。

其中包括平台层和可共用C层的设计和实现,C层处理需要高运算效率的图像处理,粒子系统等算法。

项目背景:

1、为了让App展示更酷炫,UX团队在设计中增加了大量的动画效果。

2、面对同一个效果,往往不同开发保持着不同的开发思路。最后实现的效果往往不一致。为了保持效果的一致性,需要花费大量的时间跟动效设计师沟通、调优,甚至重新开发。

解决办法:

基于可以跨Android和iOS 的C/C++开发语言,1人开发和交流,提炼和实现Android、iOS的核心效果算法。C层提供微调接口给平台层,例如气泡大小等等。

以下我们以项目中遇到的水波和气泡效果为例,讨论怎样获得可共用的C/C++代码。效果图如下:

android开发 蠕动动画 安卓动效开发_c语言移动端开发

这两种效果都具有随机性和人机交互,例如水波的波峰、波谷、振幅都是随机的;气泡是有生命的,从创建开始就有了粒子属性,还可以在用户点击的位置动态产生气泡。 避免让用户看起来像播放gif动画。

那我们接着需要思考除了用C/C++语言开发外,哪些动效算法适合做成跨平台的呢?

这里我们对Android和iOS设备做了一层抽象。做过图像处理的开发都比较了解这点:屏幕上显示的图像,都是由像素点组成的。像素点不同颜色和位置勾画出了整个图像。

而且在设备里面一般都是通过一块内存来存放像素点信息的,映射到代码里面就是一个二维数组。那刚好联想到Android和iOS中对图片的处理都会从原始的jpeg或者png文件解析到一块内存中,显示的时候通过像素合成到显存中。

具体步骤如下:(以气泡效果为例)

1、创建自定义View

这个可以做成模版,后面的需要自定义的动画,都可以一样的复用。

Android:
public class BubbleView extends View {
……
@Override
public void onDraw(Canvas canvas) {
long startTime = System.currentTimeMillis(), endTime;
//刷新内存
DRLib.showBubbles(baseBitmap, baseBitmap.getWidth(),baseBitmap.getHeight());
//更新到画布
canvas.drawBitmap(baseBitmap, srcRect, srcRect, null);
endTime = (System.currentTimeMillis() - startTime);
Log.e("TAG", "Bubble endTime =" + endTime);
handler.postDelayed(runnable, (long) (Math.max(5.0d, 50.0d - endTime)));
}
//点击交互
@Override
public boolean onTouchEvent(MotionEvent event) {
……
switch (action) {
case MotionEvent.ACTION_DOWN:
DRLib.addBubble(pos_x, pos_y);
break;
}
return true;
}
}
iOS :
class BundlesView: UIView {
……
func setupSubviews() {
BubbleOC.initBubbles(UIImage(named:"bubble"))
timer = Timer(timeInterval: 0.05, target: self, selector: #selector(self.showBubbles), userInfo: nil, repeats: true)
RunLoop.main.add(timer!, forMode:RunLoopMode.commonModes)
}
func showBubbles() {
let uiImage = BubbleOC.showBubbles((NSInteger)(self.frame.width), height:(NSInteger)(self.frame.height));
let bgColor = UIColor.init(patternImage: uiImage!);
//iOS 需要从UIImage再解析出来颜色数据,设置到背景。这里相当于做了Color Data转UIImage,再UIImage变Color Data的过程。有时间再研究有没有简化方法。
self.backgroundColor = bgColor;
}
}

以上代码大家注意一下刷新频率,我们这里把他设置成每秒钟20帧。一般系统自刷新都是50帧以上。我们需要取舍效果的功耗和流畅度。

除此之外,我们需要加算好恒定的刷新周期,在Android中有50.0d - endTime,就是减去了中间代码的运算时间。在效果算法调优的过程中,如果想得到效果运算时间,需要取多次采样的平均值。

2、创建内存

Android:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
baseBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
//baseBitmap图像合成Buffer
Bitmap bubbleBitmap = BitmapFactory.decodeResource(this.getResources(), R.mipmap.particle_bubble).copy(Bitmap.Config.ARGB_8888, true);
DRLib.initBubbles(bubbleBitmap);
//bubbleBitmap 原始图像数据Buffer
}
iOS :
@implementation BubbleOC
+ (UIImage *) initBubbles:(UIImage *)uiImage {
CGImageRef imageRef;
imageRef = uiImage.CGImage;
int width = (int)CGImageGetWidth(imageRef);
int height = (int)CGImageGetHeight(imageRef);
bubbleWidth = width;
bubbleHeight = height;
size_t bitsPerComponent;
bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
size_t bitsPerPixel;
bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
size_t bytesPerRow;
bytesPerRow = CGImageGetBytesPerRow(imageRef);
CGColorSpaceRef colorSpace;
colorSpace = CGImageGetColorSpace(imageRef);
CGBitmapInfo bitmapInfo;
bitmapInfo = CGImageGetBitmapInfo(imageRef);
bool shouldInterpolate;
shouldInterpolate = CGImageGetShouldInterpolate(imageRef);
CGColorRenderingIntent intent;
intent = CGImageGetRenderingIntent(imageRef);
CGDataProviderRef dataProvider;
dataProvider = CGImageGetDataProvider(imageRef);
CFDataRef data;
UInt8* buffer;
data = CGDataProviderCopyData(dataProvider);
buffer = (UInt8*)CFDataGetBytePtr(data);
initBubble(buffer, width, height);
CFDataRef effectedData;
effectedData = CFDataCreate(NULL, buffer, CFDataGetLength(data));
CGDataProviderRef effectedDataProvider;
effectedDataProvider = CGDataProviderCreateWithCFData(effectedData);
CGImageRef effectedCgImage;
UIImage* effectedImage;
effectedCgImage = CGImageCreate(
width, height,
bitsPerComponent, bitsPerPixel, bytesPerRow,
colorSpace, bitmapInfo, effectedDataProvider,
NULL, shouldInterpolate, intent);
effectedImage = [[UIImage alloc] initWithCGImage:effectedCgImage];
CGImageRelease(effectedCgImage);
CFRelease(effectedDataProvider);
CFRelease(effectedData);
CFRelease(data);
return effectedImage;
}
+ (UIImage*) showBubbles:(NSInteger)width height:(NSInteger)height {
UInt8* buffer;
int size = (int)(width * height << 2);
buffer = (UInt8 *)malloc(size * sizeof(UInt8));//创建像素合成内存
bubbleCycle(buffer, (int)width, (int)height);
CFDataRef effectedData;
effectedData = CFDataCreate(NULL, buffer, size);
CGDataProviderRef effectedDataProvider;
effectedDataProvider = CGDataProviderCreateWithCFData(effectedData);
CGImageRef effectedCgImage;
UIImage* effectedImage;
effectedCgImage = CGImageCreate(
width, height,
8, 32, width * 4,
CGColorSpaceCreateDeviceRGB(), kCGBitmapByteOrder32Big | kCGImageAlphaFirst, effectedDataProvider,
NULL, true, kCGRenderingIntentDefault);
effectedImage = [[UIImage alloc] initWithCGImage:effectedCgImage];
CGImageRelease(effectedCgImage);
CFRelease(effectedDataProvider);
CFRelease(effectedData);
free(buffer);
buffer = nil;
return effectedImage;
}
@end

3、C语言公用算法

由于示例效果都是全View更新,我们把对C层的处理请求,抽象出3个方法。1、初始化资源 2、更新效果 3、平台交互 4、释放资源。

我们采用粒子系统,来统一管理每个气泡的生命周期。可以先看一下简化的例子系统。

typedef struct BitmapStruct {
unsigned char *data;
int width;
int height;
} Bitmap;
typedef struct ParticleStruct{
int visible;
int life;
int size;
int cx;
int cy;
int cz;
int speedX;
int speedY;
int speedIncX;
int speedIncY;
int alpha;
int alphaInc;
int angle;
int radius;
int color;
int width;
int height;
Bitmap bitmap; //原始数据的像素的Buffer
} Particle;
extern Particle *addPaticel(int *data, int width, int height, int color) ;
//核心气泡处理算法
Particle particles[BUBBLE_MAX]; //粒子数组
//初始化环境:由于气泡效果每一个粒子的数据是同一个,我们为了节省内存,只创建一个原数据的Bitmap
void initBubble(unsigned char *data, int width, int height) {
long bitsSize = width * height * 4;
particleBitmap.data = (unsigned char *) malloc(bitsSize);
particleBitmap.width = width;
particleBitmap.height = height;
memcpy(particleBitmap.data, data, bitsSize);
}
//更新效果:每个粒子投射到画布上的像素合成处理
void drawBubble(Particle particle, unsigned char *layerBuffer, int layerWidth, int layerHeight) {
for (int y = 0; y < particleBitmap.height; y++) {
if (particle.cy + y < 0) {
break;
}
unsigned int *destLine = (unsigned int *) layerBuffer + (particle.cy + y) * layerWidth + particle.cx;
for (int x = 0; x < particleBitmap.width; x++) {
if (x + particle.cx == layerWidth - 1) {
break;
}
unsigned int srcColor = *((unsigned int *) particleBitmap.data + y * particleBitmap.width + x);
unsigned int alpha = srcColor >> 24;
unsigned int destAlpha;
switch (alpha) {//根据原始数据alpha,跟背景色混合处理
case 0:
break;
case 255:
*(destLine + x) = srcColor;
break;
default:
destAlpha = ((particle.alpha * alpha) >> 8);
*(destLine + x) = (destAlpha << 24) | (srcColor & 0x00FFFFFF);
break;
}
}
}
}

在本例的中我们的气泡图片是有透明度的,所以需要跟背景颜色做颜色混合。我们需要理解alpha的取值,0是全透明,我们就不需要在背景色上做修改。255是不透明直接用气泡的像素点颜色取代背景色。

除此之外的取值,都是要做像素点颜色合成,可以简单理解alpha值就是要往目标像素点,加本像素点多少颜色值。PNG图片就是带alpha值的图片,我们在创建显示png图片内存时一般都用GRBA8888,每个像素点占用32位。但是JPG图片我们可以只使用RGB888,每个像素点可以节省一个字节,甚至可以损失点精度设置成RGB565。

void bubbleCycle(unsigned char *layerBuffer, int layerWidth, int layerHeight) {
int i, count;
float scale, half_height;
….
for (i = 0; i < BUBBLE_MAX; i++) {
if (count == 0)
break;
if (particles[i].visible == 1) {
particles[i].life--;
particles[i].cx += particles[i].speedX;
particles[i].cy += particles[i].speedY;
particles[i].speedX += particles[i].speedIncX;
particles[i].speedY += particles[i].speedIncY;
//根据粒子属性,实现一个活泼的小粒子
drawBubble(particles[i], layerBuffer, layerWidth, layerHeight);
//粒子回收
if (particles[i].life == 0 || particles[i].cy < 0
|| particles[i].cx < -particleBitmap.width || particles[i].cx > layerWidth + particleBitmap.width) {
particles[i].visible = 0;
i--;
bubbleCount--;
}
count--;
}
}
//产生新粒子
count = random() % BUBBLE_MAX;
for (i = 0; i < count; i++) {
……
particles[i].visible = 1;
bubbleCount++;
}
}
//跟用户交互式时的动态添加粒子处理
void addBubble(int x, int y) {
……
particles[i].alpha = 155 + random() % 100;
particles[i].visible = 1;
}
//释放资源
void freeBubble() {
if (particleBitmap.data != NULL) {
free(particleBitmap.data);
particleBitmap.data = NULL;
}
}

通过上面的步骤我们实现了Android和iOS两端一致的动效。核心代码都是通过C层共用代码提供。

示例的代码,可以从以下链接下载:

https://github.com/153493932/Animation

以上都是个人思考和实践。上面的设计主要针对有较强交互和随机的动效,而且需要团队自己从零开发的时候使用。

效果固定的动效可以利用Lottie SDK,由动效工程师直接倒出Json文件播放。大家如果有问题可以随时交流。