如何实现一个跨平台UI框架-以Flutter为例(一)

688 阅读7分钟

引言

如何实现一个跨平台的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工程

截屏2024-08-13 12.29.41.png

如上图,为一个空的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。