引言
如何实现一个跨平台的UI框架呢?以Flutter这样的自渲染框架为例子,不同平台只是一个画板。在Android,用户的操作手势,比如点击事件,被FlutterView接收后,通过JNI传给Flutter的引擎,然后引擎传给Dart,我们用Dart编写代码,接受到手势,执行一些操作。此外,Flutter接受屏幕同步信号,在每一帧触发自己的渲染,最后把渲染结果展示到Android的View中。
如上,其实所谓的跨平台便是,我用平台无关的语言,比如C++,写一套排版、布局、渲染的逻辑。再写一些兼容层,把各个平台的用户操作事件等等,通过兼容层传递到我写的C++引擎中。这样,开发者可以在统一的框架中写代码,然后便可以在Android、iOS中运行。Flutter便是这样的思路,如下图为Flutter官方的架构:
这个架构中,最底下的Embedder层便是兼容层,屏蔽掉不同平台的差异性,在Android平台使用java开发,在iOS使用OC开发。上面的Engine层和Framework层使用c++、dart开发,为平台无关的语言。Engine层实现各种布局、渲染逻辑,为了开发方便,还内置了一个Dart语言的虚拟机,这样,我们便可以将大部分逻辑使用Dart快速开发,Flutter自己的Framework,包括各种Widget、动画、手势便是使用Dart开发。
想想第一次接触Flutter,由于Dart语言非常好上手,快速过一下await异步等写法,我们便开始学习Flutter这套规范,从Framework中的Widget开始,我们学习不同的Widget比如Column、Row、Image、Container等等来描述自己的页面,学习Navigator路由组件来维护页面栈,我们使用Provider等状态管理工具来管理页面的刷新。
但如果要实现一个UI框架,我们却应该从最底层开始学习Flutter。
完成图像绘制、点击事件分发
在这一部分,我们用C++简单实现一个矩形绘制,并将手势从Android传递到我们的C++引擎,实现点击矩形后,矩形换一个颜色。虽然功能上比较简单,但实现上却涉及手势的传递、绘制上屏以及JNI的使用,还是有很多东西可学的。
1.新建一个Android工程
如上图,为一个空的Android工程,页面中的“Hello Android”使用官方推荐的Compose声明式UI框架实现。我们在这一部分的目标便是在这个MainActivity上展示一个可以随着手势点击变化的矩形,这个矩形的绘制变化使用与平台无关的C++实现。
2. 使用JNI调用C++函数
我们首先在原来的工程基础上创建一个新的Native模块
Android Studio为我们自动创建了一个Java native函数,以及对应符合JNI规范的c++函数,同时把CMakeLists配置好了。
public class NativeLib {
// Used to load the 'nativelib' library on application startup.
static {
System.loadLibrary("nativelib");
}
/**
* A native method that is implemented by the 'nativelib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativelib_NativeLib_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
在app模块的build.gradle中,我们设置好依赖原生模块implementation(project(":nativelib"))
,便可以在app模块的java代码中调用NativeLib.stringFromJNI()
。这样,点击运行,Android Studio会自动编译我们的native模块,我们在app模块中通过调用原生模块的java native方法,从而调用到了c++代码。
3.绘制一个矩形并上屏
在这一步,我们需要在C++层实现绘图,最终把绘图结果显示在屏幕上。这一步相对来说复杂一些了,一般而言,对于Android开发,我们如果需要绘制一些自定义内容,往往也只会自定义View,通过画笔来画一些内容,整个过程仍然包装在Java这一层。
在Android中,官方提供了GLSurfaceView
,让我们可以在Activity中展示OpenGL绘制的内容。只需要我们实现GLSurfaceView.Renderer
接口即可,如下,在onCreate中创建。
mGLSurfaceView = GLSurfaceView(this)
mGLSurfaceView!!.setEGLContextClientVersion(2)
mGLSurfaceView!!.setRenderer(MyGLRenderer())
setContentView(mGLSurfaceView)
public class MyGLRenderer implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 在此处进行OpenGL ES的初始化操作
// 设置背景颜色、启用深度测试等等
nativeInit();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 在此处响应GLSurfaceView的尺寸变化
// 更新视口、投影矩阵等等
}
@Override
public void onDrawFrame(GL10 gl) {
// 在此处进行渲染操作
// 调用C++的渲染方法
nativeRender();
}
// 声明一个本地方法,用于调用C++的渲染逻辑
private native void nativeRender();
public static native void nativeInit();
}
在onSurfaceCreated
中,我们可以开始在C++里创建好着色器,在onDrawFrame
中,我们开始在C++中绘制内容,绘制的内容会自动上屏,下面为对应的两个native方法,createProgram
这些方法以及具体的着色器定义由于篇幅,不再下方列出,可以去本章的git地址查看。
extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativelib_MyGLRenderer_nativeInit(JNIEnv *env, jclass) {
// 初始化着色器
gProgram = createProgram(vertexShaderSource, fragmentShaderSource);
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_nativelib_MyGLRenderer_nativeRender(JNIEnv *env, jobject /* this */) {
// 使用我们定义的着色器,在屏幕中心画一个矩形
// 清除屏幕
glClearColor(1.0f, 1.0f, 1.0f, 1.0f); // 设置背景颜色为白色
glClear(GL_COLOR_BUFFER_BIT);
// 顶点数据
GLfloat vertices[] = {
-0.5f, 0.5f, // 左上角
0.5f, 0.5f, // 右上角
-0.5f, -0.5f, // 左下角
0.5f, -0.5f // 右下角
};
// 使用已经编译好的着色器程序
glUseProgram(gProgram);
// 获取顶点属性的位置
GLint posAttrib = glGetAttribLocation(gProgram, "a_Position");
GLint colorUniform = glGetUniformLocation(gProgram, "u_Color");
// 启用顶点属性数组
glEnableVertexAttribArray(posAttrib);
// 设置顶点属性指针
glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 0, vertices);
// 设置颜色
glUniform4fv(colorUniform, 1, gColor);
// 绘制矩形
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// 禁用顶点属性数组
glDisableVertexAttribArray(posAttrib);
}
这样,我们就成功在MainActivity中,绘制了一个红色矩形:
4. 完成手势传递
离我们本章的目标就差一个手势传递了,考虑一下Flutter是如何完成手势的分发的:在FlutterView中,我们能看到点击事件在这里通过androidTouchProcessor.onTouchEvent(event)
的方式,最终通过FlutterJNI的nativeDispatchPointerDataPacket
方法通知给在C++侧。
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (!this.isAttachedToFlutterEngine()) {
return super.onTouchEvent(event);
} else {
if (VERSION.SDK_INT >= 21) {
this.requestUnbufferedDispatch(event);
}
return this.androidTouchProcessor.onTouchEvent(event);
}
}
那么我们也可以仿照这个方法,将事件传递给C++层,让C++侧在收到点击事件后,如果是点击事件,则改变UI的颜色,让矩形在红、绿、蓝三种颜色中变化。
我们继承GLSurfaceView
来实现点击事件捕获传递给C++,C++侧简单的判断是点击事件,就设置当前的颜色。手指的down、up、move事件的action分别为0、1、2。
public class MyGLSurfaceView extends GLSurfaceView {
public MyGLSurfaceView(Context context) {
super(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
performClick();
onTouchEvent(event.getAction(), event.getRawX(), event.getRawY());
return true;
}
@Override
public boolean performClick() {
return super.performClick();
}
private native void onTouchEvent(int action, double x, double y);
}
GLfloat gRedColor[4] = {1.0f, 0.0f, 0.0f, 1.0f}; // 初始颜色为红色
GLfloat gGreenColor[4] = {0.0f, 1.0f, 0.0f, 1.0f};
GLfloat gBlueColor[4] = {0.0f, 0.0f, 1.0f, 1.0f};
float *currentColor = gRedColor;
int lastAction = -1;
extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativelib_MyGLSurfaceView_onTouchEvent(JNIEnv *env, jobject thiz, jint action,
jdouble x, jdouble y) {
if (lastAction == 0 && action == 1) {
// 上一次是down事件
// 这次是up事件,构成点击
if (currentColor == gRedColor) {
currentColor = gGreenColor;
} else if (currentColor == gGreenColor) {
currentColor = gBlueColor;
} else {
currentColor = gRedColor;
}
lastAction = -1;
} else {
lastAction = action;
}
}
这样,修改之前的代码,使用我们自定义的View就可以实现点击屏幕,变换矩形颜色。
mGLSurfaceView = MyGLSurfaceView(this)
5. 总结
至此,我们初步完成了一个非常简陋的跨平台的UI框架,其中渲染、点击处理都放在c++层完成。Flutter当然要比这复杂得多,其中,Flutter的渲染使用Surface,而非直接使用Android内置的GLSurfaceView,定制程度更高;此外,在Android层与c++层的通信方面,我们这个框架只是简单将一个点击事件往c++侧传递,Flutter支持双向传递;另外,Flutter在c++上层还提供了易于开发的Dart层,允许用户自定义UI,并监听不同手势、网络来刷新UI,而我们的框架只支持默认绘制的矩形,只能在收到点击时变个颜色。
但总结一下,这些差异只是复杂度上的差异,我们一步步完善,便可以逐渐向Flutter的实现靠齐,这也正是本系列文章的目的,通过实现Flutter部分功能来学习跨平台UI框架的设计。
接下来,我们会继续修改c++引擎层,逐渐将Dart虚拟机引入我们的工程,支持更多定制化功能,并一步步靠齐Flutter引擎和Flutter Framework。