记录一个高性能、高扩展的九宫格布局实现过程

3,810 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情

前言

为什么会想到做这一个选题?之前写到一篇九宫格动态列表评论的软键盘弹出效果,自动定位到指定坐标的文章【传送门】

由于我们的九宫格控件已经上线4年左右了,最近开始要大改动态模块,所以需要捡起来重新修改一下,所以顺便整理了一下当时实现的经历与坑点。

当时实现九宫格控件的时候参考了市面上的很多Demo与开源的方案,确实有很多方案,但是大多数方案也只是实现了效果,当数据多了、类型多了之后很容易发生卡顿,比较严重还有高度测量问题,图片加载混乱等等问题。

索性我就把从开始选方案到最终成型的过程记录一下,希望多大家有所启发。

由于时间久远,其中还发现有性能问题换了一种方案,当时记录的笔记也比较粗糙,没有记录完全,我尽量把我当时的思路讲清楚。

一、明确需求,实现思路

由于我们的需求比较特殊,不止是展示好友动态,还可以发布话题,指定发布圈子,这些都跟九宫格控件本身不相关,但是在列表的展示类型上也不止九宫格展示,还需要圆角图片,九宫格投票的类型,和视频的类型等等。

所以我们九宫格控件还需要能实现视频的展示,九宫格图片的展示与预览,还需要对投票的类型支持。

如果要实现九宫格的效果,其实大致就两种方案,要么使用GridView,要么使用自定义ViewGroup。

如果是详情页面还可以使用GridView的方式,但是在列表中由于需要复用的问题,导致Grid的效率相对比较差,并且还有一些特殊的展示需求,如4个图片并行排列,所以我们就直接考虑了自定义ViewGroup的方式。

如何实现自定义ViewGroup呢?其实我们实现了两个版本的迭代。一起来看看吧。

二、第一版本,直接实现

第一个版本的思路,其实就是一个类,ViewGroup在里面测量,布局,并使用一个Map容器记录每一个九宫格的宽高属性,避免复用的时候快速测量。

测量代码如下:

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (!canMeasureLayout){   //主要是在onPause的时候不要再测量了
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = 0;
        int totalWidth = width - getPaddingLeft() - getPaddingRight();
        if (mImageInfo != null && mImageInfo.size() > 0) {
            Log.e("onMeasure","进入条件,准备测量了");
            //查看缓存如果有缓存的宽高,那么直接使用,不需要再次测量,避免回收使用的时候gridWidth是回收的宽度造成测量不准确
            if (mCurPosition != -1 && WHCacheHelper.mAllWidth.get(mCurPosition) != null && WHCacheHelper.mAllWidth.get(mCurPosition) != 0) {
                gridWidth = WHCacheHelper.mAllWidth.get(mCurPosition);
                gridHeight = WHCacheHelper.mAllHeight.get(mCurPosition);
            } else {
                Log.e("onMeasure","进入条件,开始测量了");
                //没有缓存需要测量,测量完成之后需要缓存起来
                if (mImageInfo.size() == 1) {
                    //只有一张图片的时候
                    columnCount = 1;
                    rowCount = 1;
                    //说明是初始化,先尝试拿到url中的宽高
                    ImageInfo imageInfo = mImageInfo.get(0);
                    String thumbnailUrl = imageInfo.getThumbnailUrl();
                    int startIndex = thumbnailUrl.lastIndexOf("-");
                    int endIndex = thumbnailUrl.lastIndexOf(".");
                    if (startIndex != -1 && startIndex != 0 && endIndex != -1 && endIndex != 0 && endIndex - startIndex <= 10) {
                        String substring = thumbnailUrl.substring(startIndex + 1, endIndex);
                        if (!TextUtils.isEmpty(substring) && substring.contains("x")) {
                            String[] split = substring.split("x");
                            try {
                                handleWidthHeight(Integer.parseInt(split[0]), Integer.parseInt(split[1]), totalWidth);
                            } catch (NumberFormatException e) {
                                e.printStackTrace();
                                handleWidthHeight(0, 0, totalWidth);
                            }
                        } else {
                            handleWidthHeight(0, 0, totalWidth);
                        }
                    } else {
                        handleWidthHeight(0, 0, totalWidth);
                    }
                } else {
                    //如果是多张图片,计算宽度,宽都按总宽度的 1/3 计算一个grid的宽高
                    gridWidth = gridHeight = (totalWidth - gridSpacing * 2) / 3;
                }

                //缓存每一个NineGridView的宽高
                if (mCurPosition != -1) {
                    WHCacheHelper.mAllWidth.put(mCurPosition, gridWidth);
                    WHCacheHelper.mAllHeight.put(mCurPosition, gridHeight);
                }
            }
        }

        width = gridWidth * columnCount + gridSpacing * (columnCount - 1) + getPaddingLeft() + getPaddingRight();
        height = gridHeight * rowCount + gridSpacing * (rowCount - 1) + getPaddingTop() + getPaddingBottom();


        setMeasuredDimension(width, height);
    }

由于需要我们处理单个图片的情况与多个图片的情况,由于一行只展示三个Item,所以我们可以计算公共多少行,再加上间距即可计算需要测量的宽高。

主要是单个图片的测量相对麻烦,需要拿到图片链接中的宽高属性,判断是否大于控件宽度,如果大于控件宽度则需要等于控件宽度,再对高度进行比例的缩放。

    /**
     * 处理测量真正的宽高(单张图片的)
     */
    private void handleWidthHeight(int width, int height, int totalWidth) {
        if (width == 0) {
            //如果没有携带分辨率,那么就按1:1的比例
            gridWidth = singleImageSize;
            gridHeight = singleImageSize;
        } else {
            //如果携带了分辨率,那么直接赋值
            float width2 = width;
            float height2 = height;
            //根据不同的比例获取不同的宽高
            singleImagefinalWidthHeight(width, height, width2 / height2);
        }
    }

     /**
     * 根据宽高比,重新计算最终的gridWidth,gridHeight
     */
    private void singleImagefinalWidthHeight(float imgWidth, float imgHeight, float ratio) {
        singleImageRatio = ratio;
        int realImgWith = 0;
        int realImgHeight = 0;
        //设置单独照片的时候比例放大,不要写死了-指定宽度高度。设置一个最小的长度,低于这个长度就按照这个长度
        if (ratio >= 1.0f) {
            //横长竖短 - 如果大于最大值,不做处理,图片展示为最大长度算
            if (imgWidth >= singleImageSize) {
                //如果在最大和最小值之间,那么按图片的本身的宽高
                realImgWith = singleImageSize;
            } else if (imgWidth > singleImageMinSize) {
                realImgWith = (int) imgWidth;
            } else {
                //如果小于最小值,那么按最小的长度算
                realImgWith = singleImageMinSize;
            }

            gridWidth = realImgWith;
            gridHeight = (int) (realImgWith / ratio);

        } else {
            //竖长横短 - 和上面一样的逻辑
            if (imgHeight >= singleImageSize) {
                realImgHeight = singleImageSize;
            } else if (imgHeight > singleImageMinSize) {
                realImgHeight = (int) imgHeight;
            } else {
                realImgHeight = singleImageMinSize;
            }

            gridWidth = (int) (realImgHeight * ratio);
            gridHeight = realImgHeight;

        }
    }

主要需要处理的就是单个图片的宽高与比例,除了测量之外,ViewGroup最重要的就是给子View布局了:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (!canMeasureLayout) return;  //主要是在onPause的时候不要再布局了

        Boolean aBoolean = WHCacheHelper.mAllPositionLayouted.get(mCurPosition);
        if (aBoolean != null && aBoolean) {  //如果已经测量过了,有缓存标识,那么不要再次测量了
            return;
        }
        if (mImageInfo == null) {
            return;
        }
        Log.e("onLayout","进入条件,开始布局了");
        int childrenCount = mImageInfo.size();

        for (int i = 0; i < childrenCount; i++) {
            final ImageView childrenView = (ImageView) getChildAt(i);
            //先清除回收img上面的原有bitmap缓存
            childrenView.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_default_color));

            int rowNum = i / columnCount;
            int columnNum = i % columnCount;
            int left = (gridWidth + gridSpacing) * columnNum + getPaddingLeft();
            int top = (gridHeight + gridSpacing) * rowNum + getPaddingTop();
            int right = left + gridWidth;
            int bottom = top + gridHeight;
            childrenView.layout(left, top, right, bottom);

            //处理图片的加载,并动态的处理只有一张图片的时候的宽高比例
            if (mImageLoader != null) {
                mImageLoader.onDisplayImage(getContext(), childrenView, mImageInfo.get(i).thumbnailUrl);
            }

            WHCacheHelper.mAllPositionLayouted.clear();
            WHCacheHelper.mAllPositionLayouted.put(mCurPosition, true);
        }

    }

这个是根据行与列计算每一个Item的位置,并且使用图片加载引擎去加载图片。

其实比较难控制的就是对缓存类的控制

public class WHCacheHelper {

    public static Map<Integer, Integer> mAllWidth = new HashMap<>();

    public static Map<Integer, Integer> mAllHeight = new HashMap<>();
    
}

由于使用缓存很容易导致出错,导致一些情况下缓存的宽高不对,从而导致偶现的效果错乱的问题。如果去掉自定义的缓存就可以实现效果,但是滑动还是会卡顿,大致原因就是在测量中进行了复杂的逻辑校验与判断,导致耗时相对比较多,造成列表的卡顿。

所以才有了之前说的第一个版本上线之后反正滚动卡顿,才有了后面的一个版本重做。

三、第二版本优化实现,解耦逻辑

第一个版本上线的效果并不是很好,并且由于后面的版本上线了评论,投票等其他九宫格的方式,所以迭代更新了第二个版本。

我们需要把九宫格的逻辑提取出来作为一个基类的抽象类,而不同的布局类型去实现不同的布局,不止是图片,对于一些自定义的布局也能做成九宫格的形式去展示,扩展性也更好一点。

/**
 * 抽象九宫格-具体的布局和测量在这里完成
 */
public abstract class AbstractNineGridLayout<T> extends ViewGroup {

    private static final int MAX_CHILDREN_COUNT = 9;
    private int itemWidth;
    private int itemHeight;
    private int horizontalSpacing;
    private int verticalSpacing;
    private boolean singleMode;
    private boolean fourGridMode;
    private int singleWidth;
    private int singleHeight;
    private boolean singleModeOverflowScale;

    public AbstractNineGridLayout(Context context) {
        this(context, null);
    }

    public AbstractNineGridLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        if (attrs != null) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NineGridLayout);
            int spacing = a.getDimensionPixelSize(R.styleable.NineGridLayout_spacing, 0);
            horizontalSpacing = a.getDimensionPixelSize(R.styleable.NineGridLayout_horizontal_spacing, spacing);
            verticalSpacing = a.getDimensionPixelSize(R.styleable.NineGridLayout_vertical_spacing, spacing);
            singleMode = a.getBoolean(R.styleable.NineGridLayout_single_mode, true);
            fourGridMode = a.getBoolean(R.styleable.NineGridLayout_four_gird_mode, true);
            singleWidth = a.getDimensionPixelSize(R.styleable.NineGridLayout_single_mode_width, 0);
            singleHeight = a.getDimensionPixelSize(R.styleable.NineGridLayout_single_mode_height, 0);
            singleModeOverflowScale = a.getBoolean(R.styleable.NineGridLayout_single_mode_overflow_scale, true);
            a.recycle();
        }

        //优先填充布局,再执行测量和绘制
        fillChildView();
    }

    /**
     * 设置显示的数量
     */
    public void setDisplayCount(int count) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            getChildAt(i).setVisibility(i < count ? VISIBLE : GONE);
        }
    }

    /**
     * 设置单独布局的宽和高
     */
    public void setSingleModeSize(int w, int h) {
        if (w != 0 && h != 0) {
            this.singleWidth = w;
            this.singleHeight = h;
        }
    }

    /**
     * 一般用这个方法填充布局,每一个小布局的布局文件
     */
    protected void inflateChildLayout(int layoutId) {
        removeAllViews();
        for (int i = 0; i < MAX_CHILDREN_COUNT; i++) {
            LayoutInflater.from(getContext()).inflate(layoutId, this);
        }
    }

    /**
     * 返回每一个小布局的内部控件ID,用数组包装返回
     */
    @SuppressWarnings("unchecked")
    protected <V extends View> V[] findInChildren(int viewId, Class<V> clazz) {
        V[] result = (V[]) Array.newInstance(clazz, getChildCount());
        for (int i = 0; i < result.length; i++) {
            result[i] = (V) getChildAt(i).findViewById(viewId);
        }
        return result;
    }

    //子类去实现-填充布局文件
    protected abstract void fillChildView();

    //子类去实现-对布局文件赋值数据(一般专门去给adapter去调用的)
    public abstract void renderData(T data);


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();

        int notGoneChildCount = getNotGoneChildCount();

        if (notGoneChildCount == 1 && singleMode) {
            itemWidth = singleWidth > 0 ? singleWidth : widthSize;
            itemHeight = singleHeight > 0 ? singleHeight : widthSize;
            if (itemWidth > widthSize && singleModeOverflowScale) {
                itemWidth = widthSize;  //单张图片先定宽度。
                itemHeight = (int) (widthSize * 1f / singleWidth * singleHeight);  //根据宽度计算高度
            }
        } else {
            itemWidth = (widthSize - horizontalSpacing * 2) / 3;
            itemHeight = itemWidth;
        }

        //测量子布局
        measureChildren(MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(itemHeight, MeasureSpec.EXACTLY));


        if (heightMode == MeasureSpec.EXACTLY) {
            setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
        } else {
            notGoneChildCount = Math.min(notGoneChildCount, MAX_CHILDREN_COUNT);
            int height = ((notGoneChildCount - 1) / 3 + 1) * (itemHeight + verticalSpacing) - verticalSpacing + getPaddingTop() + getPaddingBottom();

            setMeasuredDimension(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int childCount = getChildCount();
        int notGoneChildCount = getNotGoneChildCount();
        int position = 0;

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) {
                continue;
            }
            int row = position / 3;
            int col = position % 3;

            if (notGoneChildCount == 4 && fourGridMode) {
                row = position / 2;
                col = position % 2;
            }

            int x = col * itemWidth + getPaddingLeft() + horizontalSpacing * col;
            int y = row * itemHeight + getPaddingTop() + verticalSpacing * row;

            child.layout(x, y, x + itemWidth, y + itemHeight);

            //最多只摆放9个
            position++;
            if (position == MAX_CHILDREN_COUNT) {
                break;
            }
        }
    }

    //获取真正显示的子布局
    private int getNotGoneChildCount() {
        int childCount = getChildCount();
        int notGoneCount = 0;
        for (int i = 0; i < childCount; i++) {
            if (getChildAt(i).getVisibility() != View.GONE) {
                notGoneCount++;
            }
        }
        return notGoneCount;
    }

}

为了方便大家观看,先把全部的代码贴出。

第二个方案和第一个方案的区别:

  1. 九宫格子View的填充方式暴露出去,让子类自由实现,可以是控件也可以是布局
  2. 把九宫格子View的数据填充方式暴露,子类自由的设置数据类型
  3. 对子View中的控件放入数组中管理,有多少个子View就有多少个数量
  4. 子类自由设置数据类型,方便对每一个子View的控制
  5. 对单独图片的宽高提取预取到对象中,没有在测量中进行耗时逻辑
  6. 对单独图片的宽高最大值进行限制,而并非一股脑的控件宽度

此方法的测量和布局方式其实是和第一种方案差不多的,只是把数据预处理放在请求数据之后了,把缓存的逻辑去掉了。因为测量效率比较高不需要缓存也能满足了。(当然如果想提高效率也能自己拓展实现宽高的缓存)

如何使用呢?

比如我们默认的图片九宫格,我们就可以直接继承这个基类。

/**
 * 默认的图片九宫格
 */
public class ImageViewNineGridLayout extends AbstractNineGridLayout<List<ImageInfo>> {

    private ImageView[] imageViews;
    private final ImageLoader mImageLoader;

    public ImageViewNineGridLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mImageLoader = new NineGlideLoader();
    }

    @Override
    protected void fillChildView() {
        inflateChildLayout(R.layout.item_image_grid);

        imageViews = findInChildren(R.id.iv_image, ImageView.class);
    }

    @Override
    public void renderData(List<ImageInfo> imageInfos) {

        setSingleModeSize(imageInfos.get(0).getImageViewWidth(), imageInfos.get(0).getImageViewHeight());

        setDisplayCount(imageInfos.size());

        for (int i = 0; i < imageInfos.size(); i++) {
            String url = imageInfos.get(i).getThumbnailUrl();

            ImageView imageView = imageViews[i];

            //使用自定义的Loader加载
            mImageLoader.onDisplayImage(getContext(), imageView, url);

            //点击事件
            setClickListener(imageView, i, imageInfos);
        }
    }

    //设置内部每一个图片的点击事件,跳转到预览页面
    private void setClickListener(ImageView imageView, int position, List<ImageInfo> imageInfos) {

        imageView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {

                List<Object> list = new ArrayList<>();
                for (ImageInfo imageInfo : imageInfos) {
                    if (imageInfo != null) {
                        list.add(imageInfo.getThumbnailUrl());
                    }
                }

                if (mListener != null) {
                    mListener.onPreview(imageViews, position, list);
                }

            }
        });
    }

    private OnPreViewListener mListener;

    public void setOnPreViewListener(OnPreViewListener listener) {
        mListener = listener;
    }

    public interface OnPreViewListener {
        void onPreview(ImageView[] imageViews, int position, List<Object> imageInfos);
    }

}

由于布局中就是一个ImageView,所以很简单,只需要提供数据,然后在数据填充的方法中使用图片加载引擎去加载图片即可。

效果如图:(本图片为本地测试数据,无任何特殊含义)

image.png

而如果是投票的类型,我们就需要添加一个布局,需要确定投票的选项与勾勾的选择逻辑。

/**
 * 新版本投票的九宫格
 */
public class BallotNineGridLayout extends AbstractNineGridLayout<List<ImageInfo>> {

    private MyOptionImageView[] imageViews;
    private final ImageLoader mImageLoader;

    //默认在xml中使用
    public BallotNineGridLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mImageLoader = new NineGlideLoader();
    }

    @Override
    protected void fillChildView() {
        inflateChildLayout(R.layout.item_ballot_image_grid);

        imageViews = findInChildren(R.id.iv_image, MyOptionImageView.class);

    }

    @Override
    public void renderData(List<ImageInfo> imageInfos) {

        setSingleModeSize(imageInfos.get(0).getImageViewWidth(), imageInfos.get(0).getImageViewHeight());

        setDisplayCount(imageInfos.size());

        for (int i = 0; i < imageInfos.size(); i++) {
            String url = imageInfos.get(i).getThumbnailUrl();

            MyOptionImageView optionImageView = imageViews[i];


            //使用自定义的Loader加载
            mImageLoader.onDisplayImage(getContext(), optionImageView, url);


            //设置点击事件
            setClickListener(optionImageView, i);
        }
    }

    //设置内部每一个图片的点击事件
    private void setClickListener(MyOptionImageView optionImageView, int position) {

        optionImageView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) mListener.onInnerClick(position);
            }
        });
    }

    private OnInnerClickListener mListener;

    public void setOnInnerClickListener(OnInnerClickListener listener) {
        mListener = listener;
    }

    public interface OnInnerClickListener {
        void onInnerClick(int innerPosition);
    }


    //获取到指定索引的投票图片控件
    public MyOptionImageView getOptionImageView(int index) {
        if (index >= imageViews.length) return null;

        return imageViews[index];
    }
}

这里我是把投票的View做了一个封装,所以跟图片九宫格有点类似,当然如果直接使用自定义的投票布局也能一样的实现的。

实现的效果大致如下:(本图片为本地测试数据,无任何特殊含义)

image.png

image.png

如果想扩展的话,可以自己继承抽象类,填充自己的layout,这样的话以后不管是什么样的布局,只要是以九宫格的方式展示,都可以使用自定义的布局方式来实现了。

四、其他注意事项

当前真正实现一个九宫格列表还有其他一些细枝末节的点,比如圆角,点击覆盖效果,预览效果。

比如我们的图片都是圆角的,并且加了点击的阴影效果。

public class NineGridViewWrapper extends CustomRoundImageView {

    private int moreNum = 0;             
    private int maskColor = 0x88000000; 

    public NineGridViewWrapper(Context context) {
        this(context, null);
    }

    public NineGridViewWrapper(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NineGridViewWrapper(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setFocusable(false);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Drawable drawable = getDrawable();
                if (drawable != null) {
                    drawable.setColorFilter(Color.GRAY, PorterDuff.Mode.MULTIPLY);
                    ViewCompat.postInvalidateOnAnimation(this);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                Drawable drawableUp = getDrawable();
                if (drawableUp != null) {
                    drawableUp.clearColorFilter();
                    ViewCompat.postInvalidateOnAnimation(this);
                }
                break;
        }

        return super.onTouchEvent(event);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
       setImageDrawable(null);
    }

   
}

比如如何在九宫格的最后一个子View上显示剩余图片数量的文本,也可以直接修改上面的类,在ImageView上面绘制。

比如选中与未选中的图片的drawable的绘制,直接在ImageView上面绘制,和上面的方案都是一样的思路去实现。

而需要注意的是,自定义Item的宽高缓存需要慎重,如果想继续优化速度,其实大家也可以加入Item宽高缓存,在测量的时候直接赋值,会稍微加快处理的速度,但是记住在onLayout的时候记得一定要重新加载数据。

还有就是在列表上下滚动的时候有复用的问题,记住图片最好在加载前清除之前的图片并展示站位图,否则可能会出现九宫格其中的一张图片显示复用的图片,如果使用得当时可以加速测量速度,如果使用不当反倒会负优化。

大致运行环境如下:(本图片为本地测试数据,无任何特殊含义)

device-2022-10-11-174731 00_00_00-00_00_30.gif

录制GIF软件帧数比较低,大家明白即可。

总结

总的来说九宫格控件的自定义并不难,主要就是ViewGroup的自定义,如何测量,如何布局子View,可以说是比较标准的测量与布局示例了,然后就是对一些子View的抽取,子View的数据赋值的抽取,就是一个比较完善的自定义布局了。

本文的核心代码都已经在文中贴出了,想要可以自取即可。

当然了,这种方案可能也只是闭门造车,还需要大家提提意见,如果你有更好的方案,或者优化的空间都也可以一起交流一下。如有错漏的地方还请指出,如果有疑问也可以在评论区大家一起讨论哦。

如果感觉本文对你有一点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。