React Native 代码阅读(十二):Yoga 底层布局库(Android)

2019-03-26

前言

React Native 并没有使用 Android 原生的布局引擎,而是使用了 Facebook 的 Yoga 布局引擎。Yoga 引擎是跨平台的,它实现了 Flexbox 布局模式。在本文中,我们就来看看如何使用 React Native 的底层布局引擎 Yoga。

关于本文的实例代码请参见 MaxieeRNLabYogaActivity。如果你觉得这个项目有所帮助,欢迎 Star!

Yoga 布局与 Android 原生布局的不同

在最初解除 Yoga 的时候,完全搞不懂它是怎么来进行布局的。原因是我对 Android 布局的概念先入为主了。

首先需要知道的是,Yoga 是一套完全独立的布局引擎,这也就是说它没有用到 Android 的布局逻辑。

为了完成这个思维转变,让我们先想想 Android 原生是如何布局的?

我们可能会写如下的布局:

<LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"/>
</LinearLayout>

在写下这段布局的时候,我们就已经在使用 Android 原生的布局逻辑了:

  • 首先我们创建了一个 LinearLayout,它是一个线性容器布局,我们指定它的大小策略为横向充满,纵向包裹内容,沿竖向进行排列
  • 在 LinearLayout 包含有一个横纵都包裹内容的 TextView
  • 这个布局在运行时,Android 原生的布局引擎会遍历布局,根据控件的大小策略来进行 onMeasure、onLayout 操作
  • 这个过程是 Android 开发的基本功,也是面试的常考内容

但是在本文中,我们要使用 Yoga 引擎来替换 Android 原生的布局引擎,因此上面的这些知识全部不会用到!我们需要将认知清空。

用 Yoga 进行布局

清空到什么程度呢?现在屏幕上只有一个坐标系。假设我们创建一个 TextView,如何将它摆到指定位置呢?通过向它指定 x、y 坐标。

Yoga 布局引擎实际上是一个数值计算引擎,它实际上是与 UI 视图无关的。

在 Yoga 中,布局的每个元素都是一个节点(YogaNode),这个 YogaNode 会代表界面上的某个元素(比如一个 TextView),但是两者没有直接关联。

比如假设我们要在屏幕上横向展示三个方块,对应的代码如下(Activity 的 onCreate 方法):

FrameLayout container = new FrameLayout(this);

/**
 * 布局纯数值计算
 */
float screenWidth = getWindowManager().getDefaultDisplay().getWidth();
float screenHeight = getWindowManager().getDefaultDisplay().getHeight();

YogaNode root = new YogaNode();
root.setWidth(screenWidth);
root.setHeight(screenHeight);
root.setFlexDirection(YogaFlexDirection.ROW);

YogaNode rect1 = new YogaNode();
rect1.setHeight(VIEW_WIDTH);
rect1.setWidth(VIEW_WIDTH);
rect1.setMargin(YogaEdge.ALL, 20);

YogaNode rect2 = new YogaNode();
rect2.setHeight(VIEW_WIDTH);
rect2.setWidth(VIEW_WIDTH);
rect2.setMargin(YogaEdge.ALL, 20);

YogaNode rect3 = new YogaNode();
rect3.setHeight(VIEW_WIDTH);
rect3.setWidth(VIEW_WIDTH);
rect3.setMargin(YogaEdge.ALL, 20);

root.addChildAt(rect1, 0);
root.addChildAt(rect2, 1);
root.addChildAt(rect3, 2);

// 给定屏幕长宽,求解屏幕元素位置
root.calculateLayout(screenWidth, screenHeight);

/**
 * Android 视图创建于定位
 */
ViewGroup.LayoutParams lp = new FrameLayout.LayoutParams(VIEW_WIDTH, VIEW_WIDTH);

View v1 = new View(this);
v1.setLayoutParams(lp);
v1.setBackgroundColor(Color.parseColor("#d50000"));

View v2 = new View(this);
v2.setLayoutParams(lp);
v2.setBackgroundColor(Color.parseColor("#ff1744"));

View v3 = new View(this);
v3.setLayoutParams(lp);
v3.setBackgroundColor(Color.parseColor("#ff5252"));

container.addView(v1);
container.addView(v2);
container.addView(v3);

v1.setX(rect1.getLayoutX());
v1.setY(rect1.getLayoutY());
v2.setX(rect2.getLayoutX());
v2.setY(rect2.getLayoutY());
v3.setX(rect3.getLayoutX());
v3.setY(rect3.getLayoutY());

setContentView(container);

其中:

  • 整个过程分为两个部分:Yoga 引擎布局纯数值计算、Android 视图创建于定位
  • 其中 YogaNode 完全是脱离 Android View 体系的,两者只要保持概念上的一致(一个控件的 Flexbox 定义好、YogaNode 设置多么大,视图控件也要设置多么大,两者保持一致)
  • 学习了 React Native 底层的 Yoga 布局引擎之后,我们再回头看 RN 的 Shadow Node 这个概念,就会有更好的理解了

这段代码执行(clone MaxieeRNLab 后编译参见效果)的效果如下:

更复杂一点的例子

有了上一节的基础后,下面让我们来看一个更复杂一些的例子(完整代码):

package com.maxieernlab.yoga;

import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import com.facebook.yoga.YogaEdge;
import com.facebook.yoga.YogaFlexDirection;
import com.facebook.yoga.YogaNode;

import java.util.ArrayList;

public class YogaActivity1 extends AppCompatActivity {
    private static final int VIEW_WIDTH = 200;

    private float screenHeight;
    private float screenWidth;

    private ArrayList<View> poolView = new ArrayList<>();
    private ArrayList<YogaNode> poolNode = new ArrayList<>();

    private String[][] colors = new String[][] {
            new String[] { "#d50000", "#ff1744", "#ff5252", "#ff8a80" },
            new String[] { "#c51162", "#f50057", "#ff4081", "#ff80ab" },
            new String[] { "#aa00ff", "#d500f9", "#e040fb", "#ea80fc" },
            new String[] { "#6200ea", "#651fff", "#7c4dff", "#b388ff" }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        FrameLayout container = new FrameLayout(this);

        screenWidth = getWindowManager().getDefaultDisplay().getWidth();
        screenHeight = getWindowManager().getDefaultDisplay().getHeight();
        log("Screen size = (%f, %f)", screenWidth, screenHeight);

        YogaNode root = new YogaNode();
        root.setWidth(screenWidth);
        root.setHeight(screenHeight);
        root.setFlexDirection(YogaFlexDirection.COLUMN);

        log("start");
        createRow1(root, 0);
        createRow1(root, 1);
        createRow1(root, 2);
        createRow1(root, 3);

        root.calculateLayout(screenWidth, screenHeight);

        for (int i = 0; i < poolView.size(); i++) {
            View v = poolView.get(i);
            YogaNode n = poolNode.get(i);
            YogaNode r = n.getOwner();
            v.setX(r.getLayoutX() + n.getLayoutX());
            v.setY(r.getLayoutY() + n.getLayoutY());
            log("v%d position=(%f, %f)",
                    i,
                    r.getLayoutX() + n.getLayoutX(),
                    r.getLayoutY() + n.getLayoutY());
            container.addView(v);
        }

        log("end");

        setContentView(container);
    }

    private void createRow1(YogaNode root, int index) {
        log("create index = " + index);
        YogaNode row = new YogaNode();
        row.setHeight(VIEW_WIDTH);
        row.setWidth(VIEW_WIDTH * 4);
        row.setFlexDirection(YogaFlexDirection.ROW);
        row.setMargin(YogaEdge.ALL, 20);

        for (int i = 0; i < 4; i++) {
            YogaNode n = new YogaNode();
            n.setWidth(VIEW_WIDTH);
            n.setHeight(VIEW_WIDTH);
            View v = createView(colors[index][i]);
            row.addChildAt(n, i);
            poolNode.add(n);
            poolView.add(v);
        }

        root.addChildAt(row, index);
    }

    private View createView(String color) {
        View v = new View(this);
        v.setBackgroundColor(Color.parseColor(color));
        ViewGroup.LayoutParams lp = new FrameLayout.LayoutParams(VIEW_WIDTH, VIEW_WIDTH);
        v.setLayoutParams(lp);
        return v;
    }

    private void log(String template, Object... objects) {
        Log.d("max-yoga", String.format(template, objects));
    }
}

效果图如下:

其中:

  • 需要注意的一点是,对于嵌套的 Node,它的 getLayoutX 和 getLayoutY 是相对于上级 Node 而言的
  • 因此如果要得到 Node 在屏幕上的绝对坐标,需要累加它所有父级的坐标

小结

至此我们就完成了对 React Native 底层布局引擎 Yoga 的学习。

有了这一基础,下一节我们就可以向 React Native 的 UI 部分发起进攻了!😆