时光荏苒,岁月如梭,不知不觉已有一年之久没写过文章,都生疏了(其实是不会写)
刚好最近有一个连线题的需求,经过连夜奋战终于给肝出来了,感觉写的也还行,就想着分享出来,于是就有了这篇文章,如果有问题还希望大家能指出来~废话不多说,先放一张效果图:
先冷静分析一波:有左右两列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连过线,需要先擦掉然后再画新的线,分别用两个列表oldLines
和newLines
记录这些线,擦掉就是把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()
两个方法,很方便有没有~
再看下最终实现效果
github地址:LineMatchingView,最后一键三连求支持!!!