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!!