【自定义View】数学连线题

作者: 这条鱼有点甜 | 来源:发表于2021-11-11 08:38 被阅读0次

    时光荏苒,岁月如梭,不知不觉已有一年之久没写过文章,都生疏了(其实是不会写)


    8cd85996d9ac394179ee3bed.jpg

    刚好最近有一个连线题的需求,经过连夜奋战终于给肝出来了,感觉写的也还行,就想着分享出来,于是就有了这篇文章,如果有问题还希望大家能指出来~废话不多说,先放一张效果图:


    效果图.png
    先冷静分析一波:有左右两列view,点击后用线连接,中途可以重新连线,所有线连接完之后比对答案,对错用不同颜色的线标记。

    实现思路

    首先确定是自定义ViewGroup,两列view的边缘中点作为线的起始点,view的宽高最好统一,方便计算坐标。另外具体业务的数据和UI界面各有不同,所以不能约束太死,要做到解耦,还得用泛型。
    github地址:https://github.com/zaaach/LineMatchingView,可以直接去看完整代码。

    敲代码

    ①定义一条线,因为只在内部使用,就用内部类即可,记录起始坐标、颜色、连接左右view的索引

    private static class Line {
        public float startX;
        public float startY;
        public float endX;
        public float endY;
        public int color;
        public int start;
        public int end;
    }
    

    ②对数据和view进行封装

    private class LinkableWrapper {
        public Line line;
        public float pointX;
        public float pointY;
        public boolean lined;
        public View view;
        public T item;
    }
    

    ③对外提供接口,用于UI和数据的绑定。这里算是借鉴了RecyclerView的adpater,为了让两列view展示的更灵活一些,增加了itemType

    public interface LinkableAdapter<T> {
        View getView(T item, ViewGroup parent, int itemType, int position);
        int getItemType(T item, int position);
        void onBindView(T item, View view, int position);
        void onItemStateChanged(T item, View view, int state, int position);
        boolean isCorrect(T left, T right, int l, int r);
    }
    
    主菜来了,自定义ViewGroup
    public class LineMatchingView<T> extends ViewGroup {
        //item state
        public static final int NORMAL  = 100;
        public static final int CHECKED = 101;
        public static final int LINED   = 102;
        public static final int CORRECT = 103;
        public static final int ERROR   = 104;
        
        private List<LinkableWrapper> leftItems;
        private List<LinkableWrapper> rightItems;
        private final List<Line> oldLines = new ArrayList<>();//需要移除的线
        private final List<Line> newLines = new ArrayList<>();//需要画的线
        private LinkableAdapter<T> linkableAdapter;
    }
    

    然后就是onMeasure()onLayout()两步走,在测量之前,先设置数据

    public LineMatchingView<T> init(@NonNull LinkableAdapter<T> adapter){
        this.linkableAdapter = adapter;
        return this;
    }
    
    public void setItems(@NonNull List<T> left, @NonNull List<T> right){
        if (linkableAdapter == null) {
            throw new IllegalStateException("LinkableAdapter must not be null, please see method setLinkableAdapter()");
        }
        leftItems = new ArrayList<>();
        rightItems = new ArrayList<>();
        addItems(left, true);
        addItems(right, false);
        resultSize = Math.min(leftItems.size(), rightItems.size());
    }
    
    private void addItems(List<T> list, boolean isLeft){
        for (int i = 0; i < list.size(); i++) {
            T item = list.get(i);
            //生成view并添加到控件
            int type = linkableAdapter.getItemType(item, i);
            View view = linkableAdapter.getView(item, this, type, i);
            addView(view);
            int index = i;
            view.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (finished) return;
                    if (isLeft) {
                        //先恢复上个点击的item状态
                        if (currLeftChecked >= 0) {
                            notifyItemStateChanged(currLeftChecked, leftItems.get(currLeftChecked).lined ? LINED : NORMAL, true);
                        }
                        if (currLeftChecked == index) {
                            currLeftChecked = -1;
                        } else {
                            currLeftChecked = index;
                            notifyItemStateChanged(index, CHECKED, true);
                            drawLineBetween(currLeftChecked, currRightChecked);
                        }
                    }else {
                        if (currRightChecked >= 0) {
                            notifyItemStateChanged(currRightChecked, rightItems.get(currRightChecked).lined ? LINED : NORMAL, false);
                        }
                        if (currRightChecked == index){
                            currRightChecked = -1;
                        }else {
                            currRightChecked = index;
                            notifyItemStateChanged(index, CHECKED, false);
                            drawLineBetween(currLeftChecked, currRightChecked);
                        }
                    }
                }
            });
            LinkableWrapper wrapper = new LinkableWrapper();
            wrapper.item = item;
            wrapper.view = view;
            if (isLeft){
                leftItems.add(wrapper);
            }else {
                rightItems.add(wrapper);
            }
        }
    }
    

    开始测量,分别测量左右两列view,计算出两列的最大宽度之和以及最大高度

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
    
        int[] measuredLeftSize = measureColumn(leftItems, widthMeasureSpec, heightMeasureSpec);
        int measuredLeftWidth = measuredLeftSize[0];
        int measuredLeftHeight = measuredLeftSize[1];
        leftMaxWidth = measuredLeftSize[0];
    
        int[] measuredRightSize = measureColumn(rightItems, widthMeasureSpec, heightMeasureSpec);
        int measuredRightWidth = measuredRightSize[0];
        int measuredRightHeight = measuredRightSize[1];
    
        int wMode = MeasureSpec.getMode(widthMeasureSpec);
        int hMode = MeasureSpec.getMode(heightMeasureSpec);
        setMeasuredDimension(
                wMode == MeasureSpec.EXACTLY ? width : measuredLeftWidth + measuredRightWidth + getPaddingLeft() + getPaddingRight() + horizontalPadding,
                hMode == MeasureSpec.EXACTLY ? height : Math.max(measuredLeftHeight, measuredRightHeight) + getPaddingTop() + getPaddingBottom());
    }
    
    private int[] measureColumn(List<LinkableWrapper> list, int widthMeasureSpec, int heightMeasureSpec){
        int measuredWidth = 0;
        int measuredHeight = 0;
        for (int i = 0; i < list.size(); i++) {
            LinkableWrapper wrapper = list.get(i);
            View child = wrapper.view;
            LayoutParams lp = child.getLayoutParams();
            if (lp != null){
                if (itemWidth > 0){
                    lp.width = itemWidth;
                }
                if (itemHeight > 0){
                    lp.height = itemHeight;
                }
            }
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            measuredWidth = Math.max(measuredWidth, child.getMeasuredWidth());
            measuredHeight += child.getMeasuredHeight() + (i > 0 ? verticalPadding : 0);
        }
        return new int[]{measuredWidth, measuredHeight};
    }
    

    测量完毕之后开始布局,同时通过接口进行view的数据绑定

    private void doLayout(List<LinkableWrapper> list, int left, int top, boolean isLeft){
        if (list == null) return;
        for (int i = 0; i < list.size(); i++) {
            LinkableWrapper wrapper = list.get(i);
            View view = wrapper.view;
            int w = view.getMeasuredWidth();
            int h = view.getMeasuredHeight();
            view.layout(left, top, left + w, top + h);
            if (linkableAdapter != null){
                linkableAdapter.onBindView(wrapper.item, view, i);
            }
            wrapper.pointX = isLeft ? left + w : left;
            wrapper.pointY = top + h / 2f;
            top += h + verticalPadding;
        }
    }
    

    最后就是关键的画线部分,需要重写dispatchDraw()方法。在画线之前,如果两边view连过线,需要先擦掉然后再画新的线,分别用两个列表oldLinesnewLines记录这些线,擦掉就是把paint的color设置透明。具体操作:先把旧的line添加到oldLines中,再从newLines中移除,这里如果两条线的起始点坐标一样就视为同一条线。

    private void drawLineBetween(int leftIndex, int rightIndex){
        if (leftIndex < 0 || rightIndex < 0) return;
        //移除旧的连线
        LinkableWrapper leftItem = leftItems.get(leftIndex);
        if (leftItem.lined){
            Line oldLine = leftItem.line;
            if (oldLine != null){
                oldLines.add(oldLine);
                setLined(oldLine.end, false, false);
                notifyItemStateChanged(oldLine.end, NORMAL, false);
            }
        }
        LinkableWrapper rightItem = rightItems.get(rightIndex);
        if (rightItem.lined){
            Line oldLine = rightItem.line;
            if (oldLine != null){
                oldLines.add(oldLine);
                setLined(oldLine.start, false, true);
                notifyItemStateChanged(oldLine.start, NORMAL, true);
            }
        }
        if (leftItem.lined || rightItem.lined) {
            for (Iterator<Line> iterator = newLines.iterator(); iterator.hasNext(); ) {
                Line line = iterator.next();
                if (line.equals(leftItem.line) || line.equals(rightItem.line)) {
                    iterator.remove();
                }
            }
        }
        //生成新的连线
        Line newLine = new Line(leftItem.pointX, leftItem.pointY, rightItem.pointX, rightItem.pointY);
        newLine.start = leftIndex;
        newLine.end = rightIndex;
        newLine.color = lineNormalColor;
        newLines.add(newLine);
        leftItem.lined = true;
        rightItem.lined = true;
        notifyItemStateChanged(leftIndex, LINED, true);
        notifyItemStateChanged(rightIndex, LINED, false);
        //重置
        currLeftChecked = -1;
        currRightChecked = -1;
        if (resultSize == newLines.size()){
            finished = true;
            checkResult();
        }
        invalidate();
        leftItem.line = newLine;
        rightItem.line = newLine;
    }
    
    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        linePaint.setColor(Color.TRANSPARENT);
        for (Line line : oldLines) {
            canvas.drawLine(line.startX, line.startY, line.endX, line.endY, linePaint);
        }
        oldLines.clear();
        for (Line line : newLines) {
            linePaint.setColor(line.color);
            canvas.drawLine(line.startX, line.startY, line.endX, line.endY, linePaint);
        }
    }
    

    连线完成之后比对答案,是否正确也是通过接口让使用者去判断,这里只需要根据对错更新线的颜色和view的状态即可

    private void checkResult() {
        for (Line line : newLines) {
            int l = line.start;
            int r = line.end;
            if (linkableAdapter != null){
                if (linkableAdapter.isCorrect(leftItems.get(l).item, rightItems.get(r).item, l, r)){
                    line.color = lineCorrectColor;
                    notifyItemStateChanged(l, CORRECT, true);
                    notifyItemStateChanged(r, CORRECT, false);
                }else {
                    line.color = lineErrorColor;
                    notifyItemStateChanged(l, ERROR, true);
                    notifyItemStateChanged(r, ERROR, false);
                }
            }
        }
    }
    

    OK、至此连线题的功能就全部实现了,使用时只需要调用init()setItems()两个方法,很方便有没有~

    再看下最终实现效果

    line_matching_view.gif
    github地址:LineMatchingView,最后一键三连求支持!!!

    相关文章

      网友评论

        本文标题:【自定义View】数学连线题

        本文链接:https://www.haomeiwen.com/subject/nkywzltx.html