淘宝Tangram框架解析

391 阅读9分钟

Tangram是淘宝开发的一款动态化UI框架,目的是为了适应淘宝对动态化运营UI的要求,做到由数据来驱动UI的展示。

Tangram中有两个很重要的概念,组件、卡片。

组件是卡片内的独立区域模块:比如一个TextView文本展示组件。

卡片是页面内的独立区域模块,他可以作为一个独立的模块来做UI展示,完成UI上的一个模块的职责,我们称之为卡片。

可以看到组件是卡片的子集。

100、集成Tangram

101、依赖引入

//tangram相关:tangram使用3.0之前的最新版本,其他直接使用最新版本
implementation 'com.alibaba.android:tangram:2.2.5@aar'

//tangram底层支持:vlayout
implementation 'com.alibaba.android:vlayout:1.2.36@aar'

//tangram虚拟视图(更灵活的视图,后面单独开篇讲)
implementation('com.alibaba.android:virtualview:1.4.6@aar') {
    transitive true
}

//tangram支持banner翻页用的
implementation 'com.alibaba.android:ultraviewpager:1.0.7.8@aar'

//tangram内部需要rxjava
implementation 'io.reactivex.rxjava2:rxjava:2.1.12'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'

102、application onCreate中注册Tangram中图片加载实现

TangramBuilder.init(context, new IInnerImageSetter() {
    @Override
    public <IMAGE extends ImageView> void doLoadImageUrl(@NonNull IMAGE view, @Nullable String url) {
        Glide.with(context).load(url).into(view);
    }
}, ImageView.class);

103、实现Trangram流程绑定组件

//创建builder来配置参数
TangramBuilder.InnerBuilder builder = TangramBuilder.newInnerBuilder(this);
//注册自己的cell
builder.registerCell(ImageTextView.class.getSimpleName(), ImageTextView.class);
builder.registerCell(SingleImageView.class.getSimpleName(), SingleImageView.class);
builder.registerCell("CustomCell", CustomCell.class, CustomCellView.class);
builder.registerCell("InterfaceCell", MyCustomTextView.class);
builder.registerCell("NoBackground", NoBackgroundView.class);

//创建引擎
mEngine = builder.build();
//绑定RecyclerView
mEngine.bindView(mBinding.rcgTangram);

initTangramOffSet();
initTangramPreload();
initTangramCellListener();

mBinding.rcgTangram.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        //在 scroll 事件中触发 engine 的 onScroll,内部会触发需要异步加载的卡片去提前加载数据
        mEngine.onScrolled();
    }
});

组件需要实现ITangramViewLifeCycle接口:

public class ImageTextView extends LinearLayout implements ITangramViewLifeCycle {
    private ImageView mImgIcon;
    private TextView mTvTitle;

    public ImageTextView(Context context) {
        super(context);
        initView();
    }

    public ImageTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
        setOrientation(VERTICAL);
        setGravity(Gravity.CENTER);
        inflate(getContext(), R.layout.cell_image_text, this);
        mImgIcon = findViewById(R.id.img_icon);
        mTvTitle = findViewById(R.id.tv_title);
    }

    @Override
    public void cellInited(BaseCell cell) {
        Log.e("tangram", "cellInited");
        setOnClickListener(cell);
        //组件内部的曝光需要设置
        if (cell.serviceManager != null) {
            ExposureSupport exposureSupport = cell.serviceManager.getService(ExposureSupport.class);
            if (exposureSupport != null) {
                exposureSupport.onTrace(this, cell, cell.type);
            }
        }
    }

    @Override
    public void postBindView(BaseCell cell) {
        String url = cell.optStringParam("imgUrl");
        Log.e("tangram", "postBindView cell = " + cell + " | url = " + url);
        Glide.with(getContext()).load(url).into(mImgIcon);
        mTvTitle.setText(cell.optStringParam("title"));
    }

    @Override
    public void postUnBindView(BaseCell cell) {
        Log.e("tangram", "postUnBindView");
    }
}

104、绑定组件事件

private void initTangramCellListener() {
        // 设置点击事件统一support类,统一处理
        mEngine.addSimpleClickSupport(new MyClickSupport());
        // 设置曝光事件统一处理
        mEngine.addExposureSupport(new MyExposureSupport());
        CardLoadSupport.setInitialPage(1);
        mEngine.addCardLoadSupport(new CardLoadSupport(new AsyncLoader() {
            // 加载首页
            @Override
            public void loadData(final Card card, @NonNull final LoadedCallback callback) {
                Log.e("TangramActivity", "loadData");
                List<BaseCell> cells = mEngine.parseComponent(getLoadData("all-cell.json"));
                callback.finish(cells);
            }
        }, new AsyncPageLoader() {

            // 加载更多
            @Override
            public void loadData(int page, @NonNull Card card,
                                 @NonNull LoadedCallback callback) {
                Log.e("TangramActivity", "loadData more page");
                List<BaseCell> cells = mEngine.parseComponent(getLoadData("all_data.json"));
                boolean hasMore = false;
                callback.finish(cells, hasMore);
            }
        }));
//        mEngine.addCardLoadSupport(mCardLoadSupport);
    }

其中MyClickSupoort处理点击事件:

public class MyClickSupport extends SimpleClickSupport {

    public MyClickSupport() {
        // 相当于是一个优化项,可以让Support完全代理组件cell的点击事件,
        // 而不用在每个cell中再实现
        setOptimizedMode(true);
    }

    /**
     * eventType返回的看起来是当前组件在卡片模块中的的position
     * @param targetView
     * @param cell
     * @param eventType
     */
    @Override
    public void defaultClick(View targetView, BaseCell cell, int eventType) {
        super.defaultClick(targetView, cell, eventType);
        String title = cell.optStringParam("title");
        String type = cell.optStringParam("type");
        Log.e("MyClickSupport", "pos ="+cell.pos + " | stringType = " + cell.stringType
                + " | defaultClick title = " + title + " | type = " + type
                + " | eventType = " + eventType);
        ToastUtils.toast(targetView.getContext(), TextUtils.isEmpty(title) ? type:title);
    }
}

MyExposureSupport处理页面和组件的曝光,比如用来做日志上报就很合适。

CardLoadSupport用来做数据异步加载和分页加载。

105、绑定数据,视图渲染

试图渲染注意只能传入JsonArray,所以要保证服务端返回的数据是JsonArray格式而不是JsonObject。

//设置数据,触发渲染
byte[] bytes = AssetsUtil.getAssetsFile(this, "all_data.json");
if (bytes != null) {
    String json = new String(bytes);
    try {
        JSONArray data = new JSONArray(json);
        mEngine.setData(data);
    } catch (JSONException e) {
        e.printStackTrace();
    }
}

106、json数据格式

数据格式就是UI,比如这里JsonArrya中的一个jsonObject就代表一个卡片(页面的一个完整UI模块,包含多个组件)。items是数据(数据中的某一项就是一个组件,type代表组件类型(这里第一个卡片container-banner就代表banner卡片样式;container-oneColumn代表一行一列的卡片),比如我们的自定义组件,虚拟组件,组件是代表卡片内的最小展示UI单元),type代表卡片的类型,style代表UI的样式(相当于我们在xml中定义的各种UI样式)

[{  "type": "container-banner",  "style": {    "margin": "[0,0,10,0]",    "pageWidth": 200,    "pageHeight": 100,    "indicatorMargin": "5",    "infinite": "true",    "indicatorImg2": "https://img.alicdn.com/tps/TB1XRNFNXXXXXXKXXXXXXXXXXXX-32-4.png",    "indicatorImg1": "https://img.alicdn.com/tps/TB16i4qNXXXXXbBXFXXXXXXXXXX-32-4.png",    "scrollMarginLeft": "10",    "indicatorGap": "2",    "indicatorHeight": "1.5",    "itemRatio": "2.654",    "scrollMarginRight": "10",    "indicatorGravity": "center",    "hGap": "20"  },  "items": [    {      "imgUrl": "https://wanandroid.com/blogimgs/942a5c62-ca87-4e7c-a93d-41ff59a15ba4.png",      "type": "SingleImageView"    },    {      "imgUrl": "https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png",      "type": "SingleImageView"    },    {      "imgUrl": "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png",      "type": "SingleImageView"    },    {      "imgUrl": "https://www.wanandroid.com/blogimgs/90c6cc12-742e-4c9f-b318-b912f163b8d0.png",      "type": "SingleImageView"    }  ]
},{
  "type": "container-oneColumn",
  "items": [
    {
      "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
      "title": "标题1",
      "type": "ImageTextView"
    },
    {
      "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
      "title": "标题2",
      "type": "ImageTextView"
    },
    {
      "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
      "title": "标题3",
      "type": "ImageTextView"
    },
    {
      "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
      "title": "标题4",
      "type": "ImageTextView"
    }
  ]
},{
  "type": "container-onePlusN",
  "style": {
    "aspectRatio": "1.778",
    "margin": "[10,0,0,0]"
  },
  "items": [
    {
      "imgUrl": "https://wanandroid.com/blogimgs/942a5c62-ca87-4e7c-a93d-41ff59a15ba4.png",
      "type": "SingleImageView"
    },
    {
      "imgUrl": "https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png",
      "type": "SingleImageView"
    },
    {
      "imgUrl": "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png",
      "type": "SingleImageView"
    },
    {
      "imgUrl": "https://www.wanandroid.com/blogimgs/90c6cc12-742e-4c9f-b318-b912f163b8d0.png",
      "type": "SingleImageView"
    }
  ]
},{
  "type": "container-banner",
  "style": {
    "margin": "[20,0,10,0]",
    "pageWidth": 200,
    "pageHeight": 100,
    "indicatorMargin": "5",
    "infinite": "true",
    "indicatorImg2": "https://img.alicdn.com/tps/TB1XRNFNXXXXXXKXXXXXXXXXXXX-32-4.png",
    "indicatorImg1": "https://img.alicdn.com/tps/TB16i4qNXXXXXbBXFXXXXXXXXXX-32-4.png",
    "scrollMarginLeft": "10",
    "indicatorGap": "2",
    "indicatorHeight": "1.5",
    "itemRatio": "2.654",
    "scrollMarginRight": "10",
    "indicatorGravity": "center",
    "hGap": "20",
    "autoScroll": 2000
  },
  "items": [
    {
      "imgUrl": "https://wanandroid.com/blogimgs/942a5c62-ca87-4e7c-a93d-41ff59a15ba4.png",
      "type": "SingleImageView"
    },
    {
      "imgUrl": "https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png",
      "type": "SingleImageView"
    },
    {
      "imgUrl": "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png",
      "type": "SingleImageView"
    },
    {
      "imgUrl": "https://www.wanandroid.com/blogimgs/90c6cc12-742e-4c9f-b318-b912f163b8d0.png",
      "type": "SingleImageView"
    }
  ]
},{
  "type": "container-oneColumn",
  "style": {
    "margin": "[20,0,10,0]"
  },
  "items": [
    {
      "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
      "title": "标题5",
      "type": "ImageTextView"
    },
    {
      "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
      "title": "标题6",
      "type": "ImageTextView"
    },
    {
      "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
      "title": "标题7",
      "type": "ImageTextView"
    },
    {
      "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
      "title": "标题8",
      "type": "ImageTextView"
    }
  ]
},{
  "type": "container-onePlusN",
  "style": {
    "aspectRatio": "1.778",
    "margin": "[20,0,0,0]"
  },
  "items": [
    {
      "imgUrl": "https://wanandroid.com/blogimgs/942a5c62-ca87-4e7c-a93d-41ff59a15ba4.png",
      "type": "SingleImageView"
    },
    {
      "imgUrl": "https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png",
      "type": "SingleImageView"
    },
    {
      "imgUrl": "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png",
      "type": "SingleImageView"
    },
    {
      "imgUrl": "https://www.wanandroid.com/blogimgs/90c6cc12-742e-4c9f-b318-b912f163b8d0.png",
      "type": "SingleImageView"
    }
  ]
},{
  "type": "container-oneColumn",
  "items": [
    {
      "type": "CustomCell",
      "text": "自定义model",
      "imageUrl" : "https://gw.alicdn.com/tfs/TB1vqF.PpXXXXaRaXXXXXXXXXXX-110-72.png"
    }
  ]
}
]

200、Tangram解析

Tangram底层基于vlayout,而这里的vlayout可以理解为对android的RecyclerView做了一个优化,对itemView减少了嵌套,相当于平铺了复杂情况下的子View。这样就保证了性能。

image.png

210、Tangram的type属性

Tangram中的type属性可以实现卡片和组件的UI布局,比如常见的margin边距、文字大小等。

201、流式布局

201.1、container-fourColumn 一行四列

"type": "container-fourColumn",
"items": [
  {
    "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
    "title": "标题1",
    "type": "ImageTextView"
  }]

截屏2023-11-08 下午2.06.49.png

201.2、container-onePlusN 一拖N

"type": "container-onePlusN",
"style": {
  "aspectRatio": "1.778",
  "margin": "[10,0,0,0]"
},

效果如下:

截屏2023-11-08 下午2.05.16.png

仅仅是改变了客户端请求server后返回的json数据,就做到了UI效果的改变。

还有包括banner、瀑布流、一拖二、一拖N的卡片样式。

300、注意事项

1、Tangram需要rxjava,所以引入tangram的时候需要增加rxjava的引入。

2、Tangram绑定data的时候需要JSONArray的格式,就是json数组,所以需要将数据封装成json数组的样式,如果是jsonObject就会报错。

比如,如果不加[]将数据变为jsonArray,则无法解析组件的type样式为一行四列container-fourColumn:

[{
  "type": "container-fourColumn",
  "items": [
    {
      "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
      "title": "标题1",
      "type": "ImageTextView"
    },
    {
      "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
      "title": "标题2",
      "type": "ImageTextView"
    },
    {
      "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
      "title": "标题3",
      "type": "ImageTextView"
    },
    {
      "imgUrl": "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg",
      "title": "标题4",
      "type": "ImageTextView"
    }
  ]
}]

400、Tangram高级用法

401、虚拟组件

首先虚拟组件是一个组件,我们从上面看到组件是在代码中通过registerCell的方式注册的。那这里的虚拟组件加了虚拟一词,就代表它是没有实体类对应的,完全是根据虚拟组件中的配置信息,用原生的方法draw出来的。所以后续线上你更改了这个虚拟组件的配置,并按照规范打包输出文件动态下发到客户端,就能保证虚拟组件的UI是可以随时变化的。

Tangram解决了布局中卡片的动态下发,可以根据json配置动态改变。但是组件是被java代码硬编码注册到了页面中的,即registerCell。所以如果要想更新组件的内容,就需要设计一套可以不依赖register注册就可以更新组件UI的方式,就是这里的虚拟组件。

其实很简单,就是Tangram设计了一套基于自定义的xml语言编写UI的算法,比如vImage、VText,用它提供的工具会把这个xml加密生成一个文件.out文件,字符串即bin中的字符串。通过动态下发这个out文件或者bin,客户端拿到了这个加密信息后,通过解密得到组件xml布局的原始信息,再通过各自原生android、iOS的绘制方法,来实现绘制组件UI的目的,这样就解决了硬编码去写组件的UI后,无法动态更新、改变组件UI的问题。

虚拟组件的特色:其实从当初淘宝团队的设计来说,是非常优秀的。完全和Compose的想法如出一辙,Compose也是谷歌通过声明式的方式,通过自己的绘制框架来实现了UI的跨平台,用户不需要关心如何绘制,只需要写好自己的UI声明即可。

编写虚拟组件xml

vvtest.xml

<?xml version="1.0" encoding="utf-8" ?>
<VHLayout
    action="${action}"
    background="${bgColor}"
    flag="flag_exposure|flag_clickable"
    layoutHeight="wrap_content"
    layoutWidth="match_parent"
    orientation="V">
 
    <VImage
        layoutGravity="h_center"
        layoutHeight="72rp"
        layoutMarginTop="10rp"
        layoutWidth="72rp"
        src="${imageUrl}"/>
 
    <NText
        layoutGravity="h_center"
        layoutHeight="wrap_content"
        layoutMarginBottom="10rp"
        layoutMarginTop="10rp"
        layoutWidth="wrap_content"
        text="${text}"/>
</VHLayout>

编译生成输出文件

打开命令行,执行 sh buildTemplate.sh,模板编译后的文件会输出到 build 路径下,其包含以下几个目录:

out目录:XML 模板编译成二进制数据的文件,其他内容都是以此为基础生成,上传到 cdn,通过模板管理后台下发的也是这里的文件; java目录:XML 模板编译成二进制数据之后的 Java 字节数组形式,可以直接拷贝到 Android 开发工程里使用,作为打底数据; sign目录:out 格式文件的 md5 码,供模板管理平台下发模板给客户端校验使用; txt目录:XML 模板编译成二进制数据之后的十六进制字符串形式,转换成二进制数据就是 java 目录下的字节数组。 在我们的示例中,分别生成了VVTEST.java、VVTest.out、VVTest.md5和VVTest.txt。

构建

首先构建一个 VafContext 对象,代码如下:

VafContext vafContext = new VafContext(mContext.getApplicationContext());

加载

加载模板数据,利用 VirtualView Tools 编译出的二进制文件,在初始化的时候加载,有两种方式,一种是直接加载二进制字节数组:

viewManager.loadBinBufferSync(VVTEST.BIN);

另一种是通过二进制文件路径加载(不推荐):

viewManager.loadBinFileSync("file:///android_asset/VVTest.out");

500、常见问题

501、绑定的组件运行报错NoSuchMethodException

java.lang.RuntimeException: java.lang.NoSuchMethodException: com.my.widget.ImageTextView.<init> [class android.content.Context]
at com.tmall.wireless.tangram.structure.ViewCreator.handleException(ViewCreator.java:74)
at com.tmall.wireless.tangram.structure.ViewCreator.create(ViewCreator.java:65)
at com.tmall.wireless.tangram.dataparser.concrete.BaseCellBinder.createView(BaseCellBinder.java:79)
at com.tmall.wireless.tangram.dataparser.concrete.PojoGroupBasicAdapter.createViewHolder(PojoGroupBasicAdapter.java:124)
at com.tmall.wireless.tangram.core.adapter.GroupBasicAdapter.onCreateViewHolder(GroupBasicAdapter.java:263)
at com.tmall.wireless.tangram.core.adapter.GroupBasicAdapter.onCreateViewHolder(GroupBasicAdapter.java:56)
at androidx.recyclerview.widget.RecyclerView$Adapter.createViewHolder(RecyclerView.java:7295)
at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6416)
at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6300)
at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6296)
at com.alibaba.android.vlayout.ExposeLinearLayoutManagerEx$LayoutState.next(ExposeLinearLayoutManagerEx.java:1628)
at com.alibaba.android.vlayout.VirtualLayoutManager$LayoutStateWrapper.next(VirtualLayoutManager.java:1143)
at com.alibaba.android.vlayout.layout.RangeGridLayoutHelper.layoutViews(RangeGridLayoutHelper.java:374)
at com.alibaba.android.vlayout.layout.BaseLayoutHelper.doLayout(BaseLayoutHelper.java:318)
at com.alibaba.android.vlayout.VirtualLayoutManager.layoutChunk(VirtualLayoutManager.java:741)
at com.alibaba.android.vlayout.ExposeLinearLayoutManagerEx.fill(ExposeLinearLayoutManagerEx.java:1161)
at com.alibaba.android.vlayout.ExposeLinearLayoutManagerEx.onLayoutChildren(ExposeLinearLayoutManagerEx.java:365)
at com.alibaba.android.vlayout.VirtualLayoutManager.onLayoutChildren(VirtualLayoutManager.java:536)
at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4309)
at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:4012)
at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4578)
at android.view.View.layout(View.java:23065)
at android.view.ViewGroup.layout(ViewGroup.java:6479)
at androidx.constraintlayout.widget.ConstraintLayout.onLayout(ConstraintLayout.java:1873)
at android.view.View.layout(View.java:23065)
at android.view.ViewGroup.layout(ViewGroup.java:6479)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:23065)
at android.view.ViewGroup.layout(ViewGroup.java:6479)
at androidx.appcompat.widget.ActionBarOverlayLayout.onLayout(ActionBarOverlayLayout.java:530)
at android.view.View.layout(View.java:23065)
at android.view.ViewGroup.layout(ViewGroup.java:6479)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:23065)
at android.view.ViewGroup.layout(ViewGroup.java:6479)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
at android.view.View.layout(View.java:23065)
at android.view.ViewGroup.layout(ViewGroup.java:6479)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at com.android.internal.policy.DecorView.onLayout(DecorView.java:825)
at android.view.View.layout(View.java:23065)
at android.view.ViewGroup.layout(ViewGroup.java:6479)
at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:3626)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3086)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2074)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8542)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1076)
at android.view.Choreographer.doCallbacks(Choreographer.java:897)
at android.view.Choreographer.doFrame(Choreographer.java:826)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1061)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:236)
at android.app.ActivityThread.main(ActivityThread.java:8134)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

Tangram在RecyclerView绑定Adapter后创建ItemView的时候,会创建你绑定的cell组件

//创建builder来配置参数
TangramBuilder.InnerBuilder builder = TangramBuilder.newInnerBuilder(this);
//注册自己的cell
builder.registerCell(ImageTextView.class.getSimpleName(), ImageTextView.class);

是通过反射来创建cell的实例,并且默认是创建参数为Context的构造方法,所以如果你自定义的View中没有默认参数为Context的构造函数,就会报错找不到方法。

at com.tmall.wireless.tangram.dataparser.concrete.BaseCellBinder.createView 截屏2023-11-07 下午9.26.16.png

at com.tmall.wireless.tangram.structure.ViewCreator.create 截屏2023-11-07 下午9.23.31.png

解决方案: 为自定义View添加默认参数为Context的构造函数即可。