持续创造,加速生长!这是我参加「日新计划 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,所以很简单,只需求供给数据,然后在数据填充的办法中运用图片加载引擎去加载图片即可。

作用如图:(本图片为本地测试数据,无任何特别意义)

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

而假如是投票的类型,咱们就需求增加一个布局,需求确定投票的选项与勾勾的选择逻辑。

/**
 * 新版别投票的九宫格
 */
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做了一个封装,所以跟图片九宫格有点相似,当然假如直接运用自定义的投票布局也能相同的完结的。

完结的作用大致如下:(本图片为本地测试数据,无任何特别意义)

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

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

假如想扩展的话,能够自己承继笼统类,填充自己的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的时分记得一定要从头加载数据。

还有便是在列表上下翻滚的时分有复用的问题,记住图片最好在加载前清除之前的图片并展现站位图,不然可能会呈现九宫格其间的一张图片显现复用的图片,假如运用得其时能够加速丈量速度,假如运用不当反倒会负优化。

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

录制GIF软件帧数比较低,咱们理解即可。

总结

总的来说九宫格控件的自定义并不难,首要便是ViewGroup的自定义,怎么丈量,怎么布局子View,能够说是比较规范的丈量与布局示例了,然后便是对一些子View的抽取,子View的数据赋值的抽取,便是一个比较完善的自定义布局了。

本文的中心代码都现已在文中贴出了,想要能够自取即可。

当然了,这种计划可能也仅仅闭门造车,还需求咱们提提意见,假如你有更好的计划,或者优化的空间都也能够一起沟通一下。如有讹夺的当地还请指出,假如有疑问也能够在谈论区咱们一起讨论哦。

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

Ok,这一期就此完结。

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