前言
众所周知,RecyclerView是一个功能强大的,用于替代ListView的控件。话虽如此,我在项目中的大多数情况下仍只是将其当做一个加强版的ListView使用,很少有深入使用其高级的功能,不过随着需求的增加,深入学习RecyclerView的高级功能也是必不可少的,因此,谨以此文记录我所见所闻的RecyclerView,方便日后复习查阅。
基础
基础使用
RecyclerView可以说是开发中的常客,相信大部分开发者都能轻松地完成基本使用下的代码,甚至由于经常使用而形成了肌肉记忆,实际上RecyclerView的基本使用确实是十分公式化的,具体可以分为以下步骤: 1. 添加依赖
dependencies {
    ...
    implementation 'com.google.android.material:material:1.2.1'
}
- 在布局中添加RecyclerView
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>
- 
编写item布局 这个item布局对应的是列表中需要展示的每个条目的布局,注意其父容器的layout_height属性的值一般不设置为match_parent,否则会导致展示结果占据大量位置,观感较差,这是一种十分容易出现的错误。 
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="60dp"
    android:gravity="center"
    android:background="#ffffff">
    <TextView
        android:id="@+id/item_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#000000"
        tools:text="item"/>
</LinearLayout>
- 
编写适配器(Adapter) 使用RecyclerView最关键的一步,可以说RecyclerView的大部分逻辑功能都在此实现。这里的关注点就比较多了。 首先是ViewHolder: 这是一种性能优化的策略,RecyclerView之所以能快速展示大量数据,ViewHolder可以说是功不可没。从字面上理解,不难看出它的功能就是容纳(hold)一个视图(View),一个视图好端端的,为什么要特意再使用ViewHolder容纳呢?举个例子就很容易理解了:当我们的列表中有一百万个视图要展示时,如果我们直接加载这一百万个视图,必然会出现OOM现象,但是使用ViewHolder则不会,这是因为使用此策略,我们仅创建很少的ViewHolder来容纳View,当我们滑动列表时,实际上就是在不断复用这些ViewHolder,极大的增加了效率。至此,如果仍存有疑惑,可以至文章末尾查看相关测试。 道理讲了一大堆,具体的使用实际上就是初始化需要展示数据的控件,然后在onBindViewHolder方法中设置对应的属性即可。 其次是三个必须重写的重要方法: - onCreateViewHolder:用于创建ViewHolder,此处加载的布局即为步骤2中创建的布局。 - onBindViewHolder:用于绑定数据和事件,参数postion代表位置。 - getItemCount:用于确认子项的数量,即列表总共有多少项,一般返回数据的大小。 具体的使用看代码也十分容易理解。 结合到代码,可以理清基本的步骤: 1、 创建一个MyAdapter类,继承RecyclerView.Adapter并定义一个内部类ViewHolder。 2、 重写onCreateViewHolder、onBindViewHolder、getItemCount方法。 3、 提供一个设置数据的方法,可以是构造方法,也可以是setData方法。 4、 其他实现,如数据变更后的刷新,根据需求定义即可。 
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
    
    //需要展示的数据保存在这里
    private List<String> mContent;
    
    public MyAdapter(List<String> content){
        mContent = content;
    }
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout,parent,false);
        return new ViewHolder(view);
    }
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        String text = mContent.get(position);
        holder.itemText.setText(text);
    }
    @Override
    public int getItemCount() {
	//返回数据的大小
        return mContent.size();
    }
    public class ViewHolder extends RecyclerView.ViewHolder {
        TextView itemText;
        ViewHolder(@NonNull View itemView) {
            super(itemView);
            itemText = itemView.findViewById(R.id.item_text);
        }
    }
}
- 
初始化RecyclerView 为RecyclerView设置布局管理器和适配器。由于RecyclerView没有ListView和GridView那样明确的布局方式,因此需要设置布局管理器,这当然也是优势之一,可以一己之力适配多种布局方式。RecyclerView提供了三种布局方式:LinearLayoutManager(线性布局)、GridLayoutManager(网格布局)和StaggeredGridLayoutManager(瀑布流布局)。 
	//初始化组件
	RecyclerView recyclerView = findViewById(R.id.recycler_view);
        //设置布局管理器(LayoutManager)
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        //设置适配器(Adapter)和数据
        List<String> content = new ArrayList<>();
        for (int i = 1; i < 11; i++){
            content.add("item " + i);
        }
        MyAdapter myAdapter = new MyAdapter(content);
        recyclerView.setAdapter(myAdapter);

调整间距与添加装饰
为了让列表看起来更美观,往往还需要对每个item调整间距或添加装饰。比较简单的方法就是在item的布局中预先设置好margin属性,这种方法在线性布局中的使用效果还不错,但在网格布局和瀑布流布局中就显得不够灵活了。这时就要用到RecyclerView.addItemDecoration(ItemDecoration decor)方法了。
添加默认的装饰
可以看到,此方法需要一个ItemDecoration对象,使用由系统提供的它的子类DividerItemDecoration即可。
//添加分割线
recyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));

默认的装饰太单调,想整点花的?可以,使用DividerItemDecoration.setDrawble(Drawable drawable)方法传入一个Drawable对象来代替默认的装饰。此处用一个简单的绿色的矩形展示效果。
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#00ff00"/>
    <size android:height="2dp" />
</shape>
//来点花里胡哨的
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
dividerItemDecoration.setDrawable(getResources().getDrawable(R.drawable.divider_green));
recyclerView.addItemDecoration(dividerItemDecoration);

自定义装饰
如果默认的装饰不足以满足需求,那么就自定义一个装饰吧。在使用默认装饰时,系统提供了DividerItemDecoration类,它是基础自ItemDecoration类的,那么我们也创建一个MyDecoration类继承它,并完善相关功能即可。 使用快捷键Ctrl+O查看需要重写的方法,可以看到,除去已废弃的和构造方法,主要有三个方法:

- getItemOffsets(),获取Item的偏移量,调整参数中outRect的相关值,可以实现类似padding的效果。
- onDraw(),用这种方法绘制的任何内容都将在绘制项目视图之前绘制,绘图的内容将出现在视图下方。
- onDrawOver(),用这种方法绘制的任何内容都将在绘制项目视图之后绘制,绘图的内容会出现在视图上方。
直接看效果更容易理解,首先是getItemOffsets()方法,为了方便观察,把值设置得大一些:
    //自定义装饰
    class MyDecoration extends RecyclerView.ItemDecoration{
        @Override
        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            //设置每个item下方、左侧和右侧的间距
            outRect.bottom = 20;
            outRect.left = 40;
            outRect.right = 60;
        }
    }
可以看到,结果类似实现了padding的效果。如果仅设置每个item的bottom间距,实现的效果与默认装饰类似。

接着看onDraw()方法,这相对复杂一些,因为涉及装饰的绘制和位置的计算,以一个简单的例子为例:
    //自定义装饰
    class MyDecoration extends RecyclerView.ItemDecoration{
        @Override
        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            //设置间距
            outRect.bottom = 20;
        }
        @Override
        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            super.onDraw(c, parent, state);
            //实例化一个蓝色的画笔
            Paint paint = new Paint();
            paint.setColor(Color.BLUE);
            //获取item的数量
            int count = parent.getChildCount();
            //此处绘制一个矩形,设置其位置属性
            //由于列表是垂直分布的,所以每个要绘制的矩形左坐标和右坐标是一致的
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            //顶部坐标和底部坐标要根据item单独计算
            for (int i = 0; i < count; i++){
                View view = parent.getChildAt(i);
                //要绘制的矩形的顶部恰好是item的底部,其底部则是此值 + 间距
                float top = view.getBottom();
                float bottom = view.getBottom() + 20;
                //绘制矩形
                c.drawRect(left,top,right,bottom,paint);
            }
        }
    }
可以看到,在每个item的底部绘制了高度为20px的蓝色矩形,灵活覆写onDraw方法可以实现更好的效果。

最后是onDrawOver()方法,此方法的使用与onDraw()方法一致,唯一的区别就是绘图的内容会出现在视图上方,如果知道图层的概念应该就不难理解。简便起见,getItemOffsets()和onDraw()方法不做修改。
        @Override
        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
            Paint paint = new Paint();
            paint.setColor(Color.RED);
            int count = parent.getChildCount();
            float left = parent.getRight() - 40;
            float right = parent.getRight();
            for (int i = 0; i < count; i++){
                View view = parent.getChildAt(i);
                float top = view.getTop();
                //会覆盖掉蓝色装饰
                float bottom = view.getBottom() + 20;
                c.drawRect(left,top,right,bottom,paint);
            }
        }
很显然,新绘制的红色矩形覆盖了部分蓝色的矩形。

分类列表
有时我们需要对列表中的item进行分类展示,比如通讯录、好友列表。想要实现此功能,无非就是在适配器中根据类型进行内容的加载。可以这样理解:之前适配器中都只加载了一种布局,即item的布局。现在我们加载两种布局,一种为标题布局,一种为内容布局,并根据要求选择性地进行加载,最终展示出来的就是我们想要的结果。
以通讯录为例,首先实现两个布局,分别用于展示联系人姓名首字母和联系人基础信息:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:tool="http://schemas.android.com/tools"
    android:padding="10dp">
    <TextView
        android:id="@+id/relation_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        android:textStyle="bold"
        android:textColor="#000000"
        tool:text="A"/>
</FrameLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:tool="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:padding="10dp">
    <ImageView
        android:id="@+id/relation_profile"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:src="@mipmap/ic_launcher_round"/>
    <TextView
        android:id="@+id/relation_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginStart="10dp"
        android:textColor="#000000"
        android:textSize="18sp"
        tool:text="张三"/>
</LinearLayout>
接着实现适配器,看似内容很多,实际上大部分与基础使用无异。分几点详细说明: 1. 为简便起见,数据采用Map<Integer,String>的类型,实际使用时建议用Bean类代替。它存储两个键值对,键FLAG_IS_TITLE对应的值指代它是否为标题,键CONTENT对应的值为它的内容。 2. 相较于基础使用,此处新增了一个方法getItemViewType(int position),这个方法就是用于返回类型的。 3. onCreateViewHolder(ViewGroup parent, int viewType)中的参数viewType指的就是上述方法中返回的int值,在此需要根据类型进行ViewHolder的创建,所以我们需要两个ViewHolder(ItemHolder和TitleHolder)。
public class RelationAdapter extends RecyclerView.Adapter<RelationAdapter.ViewHolder> {
    private List<Map<Integer,String>> mData = new ArrayList<>();
    
    //指代类型的常量	
    private static final int VIEW_TYPE_TITLE = 0;
    private static final int VIEW_TYPE_ITEM = 1;
    private int FLAG_IS_TITLE = 0;
    private int CONTENT = 1;
    public RelationAdapter(List<Map<Integer,String>> data){
        mData = data;
    }
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    	ViewHolder viewHolder = null;
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        switch (viewType){
            case VIEW_TYPE_TITLE:
                viewHolder = new TitleHolder(inflater.inflate(R.layout.relation_title,parent,false));
                break;
            case VIEW_TYPE_ITEM:
                viewHolder = new ItemHolder(inflater.inflate(R.layout.relation_item,parent,false));
                break;
        }
        return viewHolder;
    }
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        if ("true".equals(mData.get(position).get(FLAG_IS_TITLE))){
            holder.titleTv.setText(mData.get(position).get(CONTENT));
        } else {
            holder.itemTv.setText(mData.get(position).get(CONTENT));
        }
    }
    @Override
    public int getItemViewType(int position) {
        if (holder instanceof TitleHolder){
            return VIEW_TYPE_TITLE;
        } else {
            return VIEW_TYPE_ITEM;
        }
    }
    @Override
    public int getItemCount() {
        return mData.size();
    }
    public class ViewHolder extends RecyclerView.ViewHolder{
        TextView itemTv;
        TextView titleTv;
        ViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }
    public class ItemHolder extends ViewHolder {
        public ItemHolder(@NonNull View itemView) {
            super(itemView);
            itemTv = itemView.findViewById(R.id.relation_name);
        }
    }
    public class TitleHolder extends ViewHolder {
        public TitleHolder(@NonNull View itemView) {
            super(itemView);
            titleTv = itemView.findViewById(R.id.relation_title);
        }
    }
}
最后只需初始化RecyclerView并设置适配器即可:
public class ThirdActivity extends AppCompatActivity {
    private List<Map<Integer,String>> mData = new ArrayList<>();
    private Map<Integer,String> map = new HashMap<>();
    private static final int FLAG_IS_TITLE = 0;
    private static final int CONTENT = 1;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        //初始化组件
        RecyclerView recyclerView = findViewById(R.id.recycler_view);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        //初始化数据
        map = new HashMap<>();
        map.put(FLAG_IS_TITLE,"true");
        map.put(CONTENT,"L");
        mData.add(map);
        map = new HashMap<>();
        map.put(FLAG_IS_TITLE,"false");
        map.put(CONTENT,"李四");
        mData.add(map);
        map = new HashMap<>();
        map.put(FLAG_IS_TITLE,"false");
        map.put(CONTENT,"李五");
        mData.add(map);
        map = new HashMap<>();
        map.put(FLAG_IS_TITLE,"true");
        map.put(CONTENT,"Z");
        mData.add(map);
        map = new HashMap<>();
        map.put(FLAG_IS_TITLE,"false");
        map.put(CONTENT,"张三");
        mData.add(map);
        //初始化适配器
        RelationAdapter relationAdapter = new RelationAdapter(mData);
        recyclerView.setAdapter(relationAdapter);
    }
}

进阶
拖拽和滑动
以基础使用的代码为主实现此效果,下面是最终效果图:

要实现此效果,首先要认识一个新的类:ItemTouchHelper。从它的名字不难看出,它是一个辅助item触摸的帮助类。而我们要做的事,就是继承ItemTouchHelper.Callback这个抽象类,并重写其中的抽象方法,最后将其与我们的RecyclerView绑定。从整个过程看,其实与实现Adapter是类似的。
先介绍一下三个重要方法:
- getMovementFlags:此方法返回一个整数类型的标识,指定item的那些移动行为是被允许的。通过makeMovementFlags(int dragFlags, int swipeFlags)方法返回,两个参数分别指定拖拽方向和滑动方向。
- onMove:拖拽操作的回调方法,它有两个ViewHolder类型的参数,指代被拖动的ViewHolder和目标位置的ViewHolder,返回值为布尔类型,表示item是否发生了位置交换。
- onSwiped:滑动操作的回调方法,它有一个ViewHolder类型和一个int类型的参数,分别指代被滑动的ViewHolder和滑动的方向。
大概了解过三个方法后,便可以根据需求着手自定义这个类了。注释比较清晰,就不过多叙述了。
public class MyItemTouchCallback extends ItemTouchHelper.Callback {
    private final ItemTouchHelperCallback helperCallback;
    public MyItemTouchCallback(ItemTouchHelperCallback helperCallback){
        this.helperCallback = helperCallback;
    }
    /**
     * 返回一个整数类型的标识,指定Item的哪种移动行为是被允许的
     * 通过makeMovementFlags(int dragFlags, int swipeFlags)方法返回
     * dragFlags指定拖拽方向,swipeFlags指定滑动方向
     */
    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
        /**
         *  ItemTouchHelper.UP    向上
         *  ItemTouchHelper.DOWN  向下
         *  ItemTouchHelper.LEFT  向左
         *  ItemTouchHelper.RIGHT 向右
         *  ItemTouchHelper.START 从右向左
         *  ItemTouchHelper.END   从左向右
         *  如果某个值传0,表示不触发该操作,此处设置支持上下拖拽和向右滑动
         */
        return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN,ItemTouchHelper.END);
    }
    /**
     * 拖拽操作的回调
     * @param viewHolder 被拖动的ViewHolder
     * @param target     目标位置的ViewHolder
     * @return 如果item发生位置交换,返回true,否则返回false
     */
    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        helperCallback.onMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }
    /**
     * 滑动操作的回调
     * @param viewHolder 滑动的ViewHolder
     * @param direction  滑动的方向
     */
    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
        helperCallback.onItemDelete(viewHolder.getAdapterPosition());
    }
	
    /**
     * 自定义接口用于方法回调
     */
    public interface ItemTouchHelperCallback{
        //滑动删除
        void onItemDelete(int position);
        //拖拽
        void onMove(int fromPosition, int toPosition);
    }
接着,让Adapter实现ItemTouchHelperCallBack接口,并实现相关方法。
    @Override
    public void onItemDelete(int position) {
        mContent.remove(position);
        notifyItemRemoved(position);
    }
    @Override
    public void onMove(int fromPosition, int toPosition) {
        Collections.swap(mContent,fromPosition,toPosition);
        notifyItemMoved(fromPosition,toPosition);
    }
最后,将ItemTouchHelper与RecyclerView绑定即可。
 //绑定itemTouchHelper
 ItemTouchHelper.Callback callback = new MyItemTouchCallback(myAdapter);
 ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
 itemTouchHelper.attachToRecyclerView(recyclerView);
至此,本文的正片内容暂且告一段落了。
个人总结,内容难免存在纰漏,细节部分不能做到尽善尽美,请各位读者海涵!
测试部分
该部分内容用于测试或说明一些开发中遇到的问题和现象。
ViewHolder
前文提到,当RecyclerView中需要展示大量的数据时,会通过创建和复用较少的ViewHolder来提升效率。只需通过对比数据总量和onCreateViewHolder的调用次数即可证明。以基础使用部分的代码为主,为了更加明显地展示结果,数据量增加到100,并输出各种情况下onCreateViewHolder和onBindViewHolder的调用次数。
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout,parent,false);
	//输出onCreateViewHolder的调用次数
        Log.e("MyAdapter","onCreateViewHolder:" + ++create_count);
        return new ViewHolder(view);
    }
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        String text = mContent.get(position);
        holder.itemText.setText(text);
        //输出onBindViewHolder的调用次数
        Log.e("MyAdapter","onBindViewHolder:" + ++bind_count);
    }
启动应用,不进行任何操作时的输出结果:

可以看到,无论是onCreateViewHolder还是onBindViewHolder都只调用了11次,这也表示此时RecyclerView能显示11个item。
接着,滑动列表,查看输出结果:

结果显而易见,onCreateViewHolder在调用了15次后不再调用,即只创建了15个ViewHolder,而onBindViewHolder是在不断被调用的,这也是正常的结果,因为一个新的item进入屏幕,肯定是需要绑定数据和事件的。结论就是,ViewHolder确实能通过复用提高效率。至于ViewHolder的创建数量,由于没看过源码,只能说,至少从测试中可以看出,它的创建数量与RecyclerView中可以同时展示(能被用户看见)的item最大数量有关。
addItemDecoration
关于RecyclerView.addItemDecoration(ItemDecoration decor)方法,相信大家已经发现了,这个方法以add而不是set开头,这也就说明可以添加多个ItemDecoration,其内部则以数组的形式存储,那么具体的展示情况如何呢,不妨测试看看。为方便起见,测试使用DividerItemDecoration类,添加数个颜色、高度不一的矩形。
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#ff0000"/>
    <size android:height="12dp" />
</shape>
对应格式如上,分别为绿色,4dp;蓝色,8dp,红色,12dp。为RecyclerView添加上这些装饰,并观察结果
//测试
DividerItemDecoration divider1 = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
divider1.setDrawable(getResources().getDrawable(R.drawable.divider_green));
DividerItemDecoration divider2 = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
divider2.setDrawable(getResources().getDrawable(R.drawable.divider_blue));
DividerItemDecoration divider3 = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
divider3.setDrawable(getResources().getDrawable(R.drawable.divider_red));
recyclerView.addItemDecoration(divider1);
recyclerView.addItemDecoration(divider2);
recyclerView.addItemDecoration(divider3);
正序添加时,结果如图,只出现了红色和灰色(背景色)的装饰,且它们的高度一致。

再看逆序添加时的结果:
//测试
recyclerView.addItemDecoration(divider3);
recyclerView.addItemDecoration(divider2);
recyclerView.addItemDecoration(divider1);

至此,不涉及源码,但可以得出结论:RecyclerView的装饰可以添加多个,且它们有层级关系,越后添加的装饰会覆盖掉之前的装饰,并且总高度为各个装饰的高度之和。
































