Time flies, time flies, I have not written an article for a year, are out of practice (actually can’t write)Just recently, there was a need for a connection problem. After fighting all night, I finally gave the liver out. I felt that it was also ok, so I wanted to share it.First, calmly analyze the first wave: there are two columns of view on the left and right, click and connect them with lines, and you can reconnect them halfway. After all lines are connected, compare the answers and mark the right and wrong with lines of different colors.

Implementation approach

First of all, define a custom ViewGroup. The midpoint of the edge of the two columns of views is used as the starting point of the line. The width and height of the view are best unified to facilitate coordinate calculation. In addition, the data and UI interface of the specific business are different, so you can’t be too constrained. To decouple, you need generics. Github address: github.com/zaaach/Line… , you can go straight to the complete code.

Knock on the code

① Define a line, because only internal use, use the inner class can record the starting coordinates, color, connect the left and right view index

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;
}
Copy the code

(2) Encapsulate data and view

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

③ Provide external interfaces for binding UI and data. Here we can refer to the Adpater of RecyclerView. In order to make the display of two columns of view more flexible, itemType is added

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);
}
Copy the code
Here comes the main course, custom viewGroups
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<>();// The line to be removed
    private final List<Line> newLines = new ArrayList<>();// The line to draw
    private LinkableAdapter<T> linkableAdapter;
}
Copy the code

OnMeasure () and onLayout() are two steps to set the data before measuring

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);
        // Generate a view and add it to the control
        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) {
                    // Restore the state of the last click on 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); }}}Copy the code

Start measuring, measure the left and right columns of VIEW respectively, and calculate the sum of the maximum width and maximum height of the two columns

@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};
}
Copy the code

After the measurement is finished, the layout is started, and the view data is bound through the interface

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; }}Copy the code

Finally, the crucial part of the drawing is to rewrite the dispatchDraw() method. Before drawing a line, if the two views have lines, erase them first and then draw newLines. Use the oldLines and newLines lists to record these lines. Erase makes the color of the paint transparent. To do this, add the old line to oldLines and then remove it from newLines. In this case, two lines are considered to be the same line if their starting points are the same.

private void drawLineBetween(int leftIndex, int rightIndex){
    if (leftIndex < 0 || rightIndex < 0) return;
    // Remove the old cable
    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(); }}}// Generate a new line
    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);
    / / reset
    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); }}Copy the code

After the connection is completed, the answer is compared, and the user can judge whether it is correct or not through the interface. Here, only the color of the line and the state of the view need to be updated according to whether it is correct or not

private void checkResult(a) {
    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); }}}}Copy the code

Init (); setItems()

Let’s look at the final implementation

Making address:LineMatchingView, one key three support!!