效果图
全部正确:
有对有错:
结果展示,纯黑色:
支持图片:
实现思路
仔细分析可以发现,连线题的布局可以分为两部分,一个是左右两列矩形,另一个是他们之间的连线。
每个矩形的宽高都一样,或者等比例,这样利于给他们定位,添加矩形时使用ViewGroup#ddView(View child, LayoutParams params)方法,我们通过LayoutParams参数来控制每个矩形的位置。
为了方便添加矩形,这里我们的自定义布局继承自RelativeLayout。
public class LinkLineView extends RelativeLayout {
...
}
接下来说连线,连线我们通过记录他们的起点和终点数据,然后调用View#invalidate方法,在ViewGgroup#dispatchDraw()方法里面通过canvas.drawLine()方法进行绘制。
我们假设线都是从左向右连的,起点就是左边矩形右边距的中点,终点就是右边矩形左边距的中点。在添加矩形的时候我们可以知道每个矩形的具体参数,所以所有连线的起点和终点的数据我们是知道的,接着就是如何表示一根线的问题。
在所有连线完成之前,连线是可以取消掉的;在所有连线完成后,我们需要用连线结果跟正确结果进行比对的,所以我们需要针对每次连线定义一个数据结构,具体如下:
public class LinkLineBean {
/**
* 直线的横纵坐标
*/
private float startX;
private float startY;
private float endX;
private float endY;
public LinkLineBean(float startX, float startY, float endX, float endY) {
this.startX = startX;
this.startY = startY;
this.endX = endX;
this.endY = endY;
}
// 省略getter和setter方法
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof LinkLineBean)) {
return false;
}
LinkLineBean that = (LinkLineBean) o;
return (Float.compare(that.startX, startX) == 0 &&
Float.compare(that.startY, startY) == 0 &&
Float.compare(that.endX, endX) == 0 &&
Float.compare(that.endY, endY) == 0)
|| (Float.compare(that.startX, endX) == 0 &&
Float.compare(that.startY, endY) == 0 &&
Float.compare(that.endX, startX) == 0 &&
Float.compare(that.endY, startY) == 0);
}
@Override
public int hashCode() {
return Objects.hash(startX, startY, endX, endY);
}
}
这里省略了一些不必要的代码。
重写equals和hashCode方法是为了比较是否是同一条连线。如果连线A的起点等于连线B的终点,连线A的终点等于连线B的起点,我们就认为他们是同一条连线,这个也符合我们的常识,同一条线从左向右连和从右向左连是一样的。
核心思路说完了,剩下的就是一些细节处理了。
源码实现
连线题父容器
/**
* @Description: 连线题的父容器
* @Version
*/
public class LinkLineView extends RelativeLayout {
private static final String TAG = LinkLineView.class.getSimpleName();
private Context context;
private List<LinkDataBean> allList = new ArrayList<>();
private List<LinkDataBean> leftList = new ArrayList<>();
private List<LinkDataBean> rightList = new ArrayList<>();
private int size;
private int cellHeight;
private int cellWidth;
private int marginLeft;
private int marginRight;
private int marginBottom;
private List<View> leftTvs = new ArrayList<>();
private List<View> rightTvs = new ArrayList<>();
boolean leftSelected;
boolean rightSelected;
View tvLeftSelected;
View tvRightSelected;
private List<LinkLineBean> linkLineBeanList = new ArrayList<>();
private List<LinkLineBean> newLinkLineBeanList = new ArrayList<>();
// 是否可点击
private boolean isEnabled = true;
private OnChoiceResultListener onChoiceResultListener;
private boolean analysisMode;
public LinkLineView(@NonNull Context context) {
super(context);
init(context);
}
public LinkLineView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public LinkLineView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public LinkLineView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
public void setOnChoiceResultListener(OnChoiceResultListener onChoiceResultListener) {
this.onChoiceResultListener = onChoiceResultListener;
}
private void init(Context context) {
this.context = context;
}
/**
* 练习
*
* @param linkDataBeanList
*/
public void setData(List<LinkDataBean> linkDataBeanList) {
if (linkDataBeanList == null || linkDataBeanList.size() == 0) {
return;
}
this.allList = linkDataBeanList;
// 将数据分为两列
for (LinkDataBean item : allList) {
if (0 == item.getCol()) {
leftList.add(item);
} else {
rightList.add(item);
}
}
// 将数据根据行号排序,避免数据错乱
Collections.sort(leftList, (o1, o2) -> o1.getRow() - o2.getRow());
Collections.sort(rightList, (o1, o2) -> o1.getRow() - o2.getRow());
LogUtils.e(TAG, "leftList:" + leftList);
LogUtils.e(TAG, "rightList:" + rightList);
size = Math.min(leftList.size(), rightList.size());
// 是否是图片类型,图片类型的话,高度跟TextView不一致
boolean isImageType = false;
for (LinkDataBean item : linkDataBeanList) {
if ("1".equals(item.getType())) {
isImageType = true;
break;
}
}
float ratioW = 0.0f;
if (isImageType) {
ratioW = 400 / 1080.0f;
cellWidth = (int) (ScreenUtils.getScreenW(context) * ratioW);
cellHeight = (int) (cellWidth * 280 / 400.0f);
} else { // TextView类型
ratioW = 400 / 1080.0f;
cellWidth = (int) (ScreenUtils.getScreenW(context) * ratioW);
cellHeight = (int) (cellWidth * 180 / 400.0f);
}
marginLeft = 0;
marginRight = 0;
marginBottom = ScreenUtils.dip2px(context, 20);
addLeftView();
addRightView();
}
/**
* 练习
* 全部黑色
*
* @param linkDataBeanList
*/
public void justShowResult(List<LinkDataBean> linkDataBeanList) {
this.analysisMode = true;
setData(linkDataBeanList);
// view绘制完成后才能获取到宽高
this.post(() -> {
List<LinkLineBean> resultList = getResultList();
// 禁止点击事件
isEnabled = false;
newLinkLineBeanList = new ArrayList<>();
for (int i = 0; i < resultList.size(); i++) {
// 改变连线的颜色
resultList.get(i).setColorString(LinkLineBean.COLOR_BLACK);
// 改变边框的颜色
leftTvs.get(i).setBackground(context.getResources().getDrawable(R.drawable.bg_black_round_10dp));
if (leftTvs.get(i) instanceof RoundedImageView) {
((RoundedImageView) leftTvs.get(i)).setBorderColor(Color.BLACK);
}
rightTvs.get(i).setBackground(context.getResources().getDrawable(R.drawable.bg_black_round_10dp));
if (rightTvs.get(i) instanceof RoundedImageView) {
((RoundedImageView) rightTvs.get(i)).setBorderColor(Color.BLACK);
}
newLinkLineBeanList.add(resultList.get(i));
}
invalidate();
});
}
private void addLeftView() {
for (int i = 0; i < leftList.size(); i++) {
LinkDataBean bean = leftList.get(i);
View view;
if ("1".equals(bean.getType())) {
view = generateImageView(bean);
} else {
view = generateTextView(bean);
}
OnClickListener onClickListener = v -> {
if (analysisMode) {
return;
}
if (!isEnabled) {
return;
}
if (tvLeftSelected != v) {
resetLeftTvStatus();
}
v.setSelected(true);
if (v instanceof RoundedImageView) {
((RoundedImageView) v).setBorderColor(Color.parseColor("#1391EB"));
}
leftSelected = true;
tvLeftSelected = v;
if (rightSelected) {
resetTvStatus();
drawLinkLine();
}
};
view.setOnClickListener(onClickListener);
// 布局
LayoutParams lp = new LayoutParams(cellWidth, cellHeight);
lp.leftMargin = marginLeft;
lp.topMargin = i * (cellHeight + marginBottom);
addView(view, lp);
leftTvs.add(view);
}
}
private void addRightView() {
for (int i = 0; i < rightList.size(); i++) {
LinkDataBean bean = rightList.get(i);
View view;
if ("1".equals(bean.getType())) {
view = generateImageView(bean);
} else {
view = generateTextView(bean);
}
OnClickListener onClickListener = v -> {
if (analysisMode) {
return;
}
if (!isEnabled) {
return;
}
if (tvRightSelected != v) {
resetRightTvStatus();
}
v.setSelected(true);
if (v instanceof RoundedImageView) {
((RoundedImageView) v).setBorderColor(Color.parseColor("#1391EB"));
}
rightSelected = true;
tvRightSelected = v;
if (leftSelected) {
resetTvStatus();
drawLinkLine();
}
};
view.setOnClickListener(onClickListener);
// 布局
LayoutParams lp = new LayoutParams(cellWidth, cellHeight);
lp.rightMargin = marginRight;
lp.topMargin = i * (cellHeight + marginBottom);
lp.addRule(ALIGN_PARENT_RIGHT);
addView(view, lp);
rightTvs.add(view);
}
}
private void resetLeftTvStatus() {
for (View item : leftTvs) {
item.setSelected(false);
if (item instanceof RoundedImageView) {
((RoundedImageView) item).setBorderColor(Color.TRANSPARENT);
}
}
}
private void resetRightTvStatus() {
for (View item : rightTvs) {
item.setSelected(false);
if (item instanceof RoundedImageView) {
((RoundedImageView) item).setBorderColor(Color.TRANSPARENT);
}
}
}
private void resetTvStatus() {
resetLeftTvStatus();
resetRightTvStatus();
}
/**
* 绘制连线
*/
private void drawLinkLine() {
if (tvLeftSelected == null || tvRightSelected == null) {
return;
}
// 从TextView上获取对应的坐标,进而确定连线的起点和终点的位置
float startX = tvLeftSelected.getRight();
float startY = (tvLeftSelected.getTop() + tvLeftSelected.getBottom()) / 2.0f;
float endX = tvRightSelected.getLeft();
float endY = (tvRightSelected.getTop() + tvRightSelected.getBottom()) / 2.0f;
LogUtils.e(TAG, "startX:" + startX + ", startY:" + startY + ", endX:" + endX + ", endY:" + endY);
if (linkLineBeanList == null) {
linkLineBeanList = new ArrayList<>();
}
LogUtils.e(TAG, "before remove:" + linkLineBeanList);
newLinkLineBeanList = new ArrayList<>();
for (LinkLineBean item : linkLineBeanList) {
newLinkLineBeanList.add(item);
}
// 在已绘制好的连线中,去除起点或终点相同的线
Iterator<LinkLineBean> iterator = newLinkLineBeanList.iterator();
while (iterator.hasNext()) {
LinkLineBean bean = iterator.next();
if (bean != null) {
if ((startX == bean.getStartX() && startY == bean.getStartY())
|| (startX == bean.getEndX() && startY == bean.getEndY())
|| (endX == bean.getStartX() && endY == bean.getStartY())
|| (endX == bean.getEndX() && endY == bean.getEndY())) {
iterator.remove();
}
}
}
LogUtils.e(TAG, "after remove:" + newLinkLineBeanList);
LinkLineBean bean = new LinkLineBean(startX, startY, endX, endY);
int leftIndex = -1;
for (int i = 0; i < leftTvs.size(); i++) {
if (tvLeftSelected == leftTvs.get(i)) {
leftIndex = i;
break;
}
}
bean.setLeftIndex(leftIndex);
int rightIndex = -1;
for (int i = 0; i < rightTvs.size(); i++) {
if (tvRightSelected == rightTvs.get(i)) {
rightIndex = i;
break;
}
}
bean.setRightIndex(rightIndex);
newLinkLineBeanList.add(bean);
LogUtils.e(TAG, "after add:" + newLinkLineBeanList);
// 重置临时变量状态
leftSelected = false;
rightSelected = false;
tvLeftSelected = null;
tvRightSelected = null;
// 检查是否所有连线均已完成
if (newLinkLineBeanList.size() >= size) {
isEnabled = false;
verifyResult();
}
// 触发dispatchDraw方法,绘制连线
invalidate();
}
private void verifyResult() {
/**
* 更新UI,标记出正确的和错误的连线
*/
drawSelectedLinkLine();
boolean isRight = true;
for (LinkLineBean item : newLinkLineBeanList) {
if (!item.isRight()) {
isRight = false;
break;
}
}
String yourAnswer = "";
if (!ListUtils.isEmpty(newLinkLineBeanList)) {
Type type = new TypeToken<ArrayList<LinkLineBean>>() {
}.getType();
try {
yourAnswer = new Gson().toJson(newLinkLineBeanList, type);
} catch (Exception e) {
e.printStackTrace();
}
}
if (onChoiceResultListener != null) {
onChoiceResultListener.onResultSelected(isRight, yourAnswer);
}
}
/**
* 将选择的结果绘制出来,有对有错那种
*/
private void drawSelectedLinkLine() {
List<LinkLineBean> resultList = getResultList();
LogUtils.e(TAG, "resultList:" + resultList);
for (int i = 0; i < newLinkLineBeanList.size(); i++) {
newLinkLineBeanList.get(i).setRight(resultList.contains(newLinkLineBeanList.get(i)));
// 改变连线的颜色
newLinkLineBeanList.get(i).setColorString(newLinkLineBeanList.get(i).isRight() ? LinkLineBean.COLOR_RIGHT : LinkLineBean.COLOR_WRONG);
// 改变边框的颜色
int leftIndex = newLinkLineBeanList.get(i).getLeftIndex();
if (leftIndex >= 0 && leftIndex < leftTvs.size()) {
leftTvs.get(leftIndex).setBackground(context.getResources().getDrawable(newLinkLineBeanList.get(i).isRight() ? R.drawable.bg_link_line_green : R.drawable.bg_link_line_red));
if (leftTvs.get(leftIndex) instanceof RoundedImageView) {
((RoundedImageView) leftTvs.get(leftIndex)).setBorderColor(newLinkLineBeanList.get(i).isRight() ? ContextCompat.getColor(context, R.color.answer_right) : ContextCompat.getColor(context, R.color.answer_wrong));
}
}
int rightIndex = newLinkLineBeanList.get(i).getRightIndex();
if (rightIndex >= 0 && rightIndex < rightTvs.size()) {
rightTvs.get(rightIndex).setBackground(context.getResources().getDrawable(newLinkLineBeanList.get(i).isRight() ? R.drawable.bg_link_line_green : R.drawable.bg_link_line_red));
if (rightTvs.get(rightIndex) instanceof RoundedImageView) {
((RoundedImageView) rightTvs.get(rightIndex)).setBorderColor(newLinkLineBeanList.get(i).isRight() ? ContextCompat.getColor(context, R.color.answer_right) : ContextCompat.getColor(context, R.color.answer_wrong));
}
}
}
}
/**
* 获取正确的连线数据
*
* @return
*/
private List<LinkLineBean> getResultList() {
List<LinkLineBean> resultList = new ArrayList<>(size);
for (int i = 0; i < leftTvs.size(); i++) {
// 从TextView上获取对应的起点坐标
float startX = leftTvs.get(i).getRight();
float startY = (leftTvs.get(i).getTop() + leftTvs.get(i).getBottom()) / 2.0f;
LinkDataBean leftBean = leftList.get(i);
for (int j = 0; j < rightList.size(); j++) {
if (leftBean.getQ_num() == rightList.get(j).getQ_num()) {
float endX = rightTvs.get(j).getLeft();
float endY = (rightTvs.get(j).getTop() + rightTvs.get(j).getBottom()) / 2.0f;
LinkLineBean linkLineBean = new LinkLineBean(startX, startY, endX, endY);
resultList.add(linkLineBean);
}
}
}
return resultList;
}
private TextView generateTextView(LinkDataBean bean) {
TextView textView = new TextView(context);
textView.setTextColor(ContextCompat.getColor(context, R.color.black));
textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
textView.setGravity(Gravity.CENTER);
textView.setMaxLines(2);
textView.setEllipsize(TextUtils.TruncateAt.END);
textView.setBackground(context.getResources().getDrawable(R.drawable.selector_link_line));
textView.setTag(bean.getQ_num());
textView.setText(bean.getContent());
return textView;
}
private RoundedImageView generateImageView(LinkDataBean bean) {
RoundedImageView riv = new RoundedImageView(context);
riv.setScaleType(ImageView.ScaleType.CENTER_CROP);
riv.setCornerRadius(ScreenUtils.dip2px(context, 10));
riv.setBorderWidth(ScreenUtils.dip2px(context, 2) * 1.0f);
riv.setBorderColor(Color.TRANSPARENT);
riv.mutateBackground(true);
riv.setImageDrawable(context.getResources().getDrawable(R.drawable.selector_link_line));
Glide.with(riv).load(bean.getContent()).into(riv);
return riv;
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
LogUtils.e(TAG, "dispatchDraw");
if (linkLineBeanList == null) {
linkLineBeanList = new ArrayList<>();
}
if (newLinkLineBeanList == null) {
newLinkLineBeanList = new ArrayList<>();
}
// 先清除掉原有绘制的线
for (LinkLineBean item : linkLineBeanList) {
if (item != null) {
Paint paint = new Paint();
paint.setColor(Color.TRANSPARENT);
paint.setStrokeWidth(ScreenUtils.dip2px(context, 2));
canvas.drawLine(item.getStartX(), item.getStartY(), item.getEndX(), item.getEndY(), paint);
}
}
for (LinkLineBean item : newLinkLineBeanList) {
if (item != null) {
Paint paint = new Paint();
paint.setColor(Color.parseColor(item.getColorString()));
paint.setStrokeWidth(ScreenUtils.dip2px(context, 2));
canvas.drawLine(item.getStartX(), item.getStartY(), item.getEndX(), item.getEndY(), paint);
}
}
linkLineBeanList.clear();
for (LinkLineBean item : newLinkLineBeanList) {
linkLineBeanList.add(item);
}
}
public interface OnChoiceResultListener {
void onResultSelected(boolean correct, String yourctAnswer);
}
}
文本连线Activity
public class LinkLineTextActivity extends AppCompatActivity {
@BindView(R.id.link_line_view)
LinkLineView linkLineView;
@BindView(R.id.fl_link_line)
FrameLayout flLinkLine;
@BindView(R.id.tv_result)
TextView tvResult;
public static void actionStart(Context context) {
Intent starter = new Intent(context, LinkLineTextActivity.class);
context.startActivity(starter);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_link_line_text);
ButterKnife.bind(this);
List<LinkDataBean> list = MockDataUtil.getInstance().mockLinkLineData(this, 0);
linkLineView.setData(list);
linkLineView.setOnChoiceResultListener((correct, yourAnswer) -> {
// 结果
StringBuilder sb = new StringBuilder();
sb.append("正确与否:");
sb.append(correct);
sb.append("\n");
tvResult.setText(sb.toString());
});
}
}
图片连线Activity
public class LinkLineImageActivity extends AppCompatActivity {
@BindView(R.id.link_line_view)
LinkLineView linkLineView;
@BindView(R.id.fl_link_line)
FrameLayout flLinkLine;
@BindView(R.id.tv_result)
TextView tvResult;
public static void actionStart(Context context) {
Intent starter = new Intent(context, LinkLineImageActivity.class);
context.startActivity(starter);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_link_line_image);
ButterKnife.bind(this);
List<LinkDataBean> list = MockDataUtil.getInstance().mockLinkLineData(this, 1);
linkLineView.setData(list);
linkLineView.setOnChoiceResultListener((correct, yourAnswer) -> {
// 结果
StringBuilder sb = new StringBuilder();
sb.append("正确与否:");
sb.append(correct);
sb.append("\n");
tvResult.setText(sb.toString());
});
}
}
ModeActivity
public class LinkLineShowModeActivity extends AppCompatActivity {
@BindView(R.id.link_line_view)
LinkLineView linkLineView;
@BindView(R.id.fl_link_line)
FrameLayout flLinkLine;
public static void actionStart(Context context) {
Intent starter = new Intent(context, LinkLineShowModeActivity.class);
context.startActivity(starter);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_link_line_show_mode);
ButterKnife.bind(this);
List<LinkDataBean> list = MockDataUtil.getInstance().mockLinkLineData(this,0);
linkLineView.justShowResult(list);
}
}
MainActivity
public class MainActivity extends AppCompatActivity {
@BindView(R.id.btn_0)
Button btn0;
@BindView(R.id.btn_1)
Button btn1;
@BindView(R.id.btn_2)
Button btn2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
@OnClick({R.id.btn_0, R.id.btn_1, R.id.btn_2})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.btn_0:
LinkLineTextActivity.actionStart(MainActivity.this);
break;
case R.id.btn_1:
LinkLineImageActivity.actionStart(MainActivity.this);
break;
case R.id.btn_2:
LinkLineShowModeActivity.actionStart(MainActivity.this);
break;
}
}
}
连线原数据
/**
* @Description: 连线的原数据
* @Version
*/
public class LinkDataBean {
/**
* content : chair
* q_num : 0
* type : 0
* col : 0
* row : 0
*/
private String content;
private int q_num;
/**
* 0:text文本
* 1:图片
*/
private String type;
private int col;
private int row;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getQ_num() {
return q_num;
}
public void setQ_num(int q_num) {
this.q_num = q_num;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public int getCol() {
return col;
}
public void setCol(int col) {
this.col = col;
}
public int getRow() {
return row;
}
public void setRow(int row) {
this.row = row;
}
@Override
public String toString() {
return "LinkDataBean{" +
"content='" + content + '\'' +
", q_num=" + q_num +
", type='" + type + '\'' +
", col=" + col +
", row=" + row +
'}';
}
}
LinkLine对象
/**
* @Description: 表示LinkLine对象
* @Version
*/
public class LinkLineBean {
public static final String COLOR_BLACK = "#ff000000";
public static final String COLOR_BLUE = "#1391EB";
public static final String COLOR_RIGHT = "#ff00deab";
public static final String COLOR_WRONG = "#ffff7c64";
/**
* 直线的横纵坐标
*/
private float startX;
private float startY;
private float endX;
private float endY;
private String colorString = COLOR_BLUE;
private boolean isRight;
private int leftIndex = -1;
private int rightIndex = -1;
public LinkLineBean(float startX, float startY, float endX, float endY) {
this.startX = startX;
this.startY = startY;
this.endX = endX;
this.endY = endY;
}
public float getStartX() {
return startX;
}
public void setStartX(float startX) {
this.startX = startX;
}
public float getStartY() {
return startY;
}
public void setStartY(float startY) {
this.startY = startY;
}
public float getEndX() {
return endX;
}
public void setEndX(float endX) {
this.endX = endX;
}
public float getEndY() {
return endY;
}
public void setEndY(float endY) {
this.endY = endY;
}
public String getColorString() {
return colorString;
}
public void setColorString(String colorString) {
this.colorString = colorString;
}
public boolean isRight() {
return isRight;
}
public void setRight(boolean right) {
isRight = right;
}
public int getLeftIndex() {
return leftIndex;
}
public void setLeftIndex(int leftIndex) {
this.leftIndex = leftIndex;
}
public int getRightIndex() {
return rightIndex;
}
public void setRightIndex(int rightIndex) {
this.rightIndex = rightIndex;
}
@Override
public String toString() {
return "LinkLineBean{" +
"startX=" + startX +
", startY=" + startY +
", endX=" + endX +
", endY=" + endY +
", colorString='" + colorString + '\'' +
", isRight=" + isRight +
", leftIndex=" + leftIndex +
", rightIndex=" + rightIndex +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof LinkLineBean)) {
return false;
}
LinkLineBean that = (LinkLineBean) o;
return (Float.compare(that.startX, startX) == 0 &&
Float.compare(that.startY, startY) == 0 &&
Float.compare(that.endX, endX) == 0 &&
Float.compare(that.endY, endY) == 0)
|| (Float.compare(that.startX, endX) == 0 &&
Float.compare(that.startY, endY) == 0 &&
Float.compare(that.endX, startX) == 0 &&
Float.compare(that.endY, startY) == 0);
}
@Override
public int hashCode() {
return Objects.hash(startX, startY, endX, endY);
}
}
数据工具类
/**
* @Description: 模拟数据的
* @Version
*/
public class MockDataUtil {
private MockDataUtil() {
}
private static final class MockDataUtilHolder {
private static final MockDataUtil INSTANCE = new MockDataUtil();
}
public static MockDataUtil getInstance() {
return MockDataUtilHolder.INSTANCE;
}
/**
* 从assets中读取对应的json文件
*
* @param context
* @param assetsName
* @return
*/
private String getJsonFromAssets(Context context, String assetsName) {
if (TextUtils.isEmpty(assetsName)) {
return null;
}
String jsonString = "";
try {
StringBuffer sb = new StringBuffer();
InputStream is = null;
is = context.getAssets().open(assetsName);
int len = -1;
byte[] buf = new byte[is.available()];//为了解决部分中文乱码问题,一次读取所有的
while ((len = is.read(buf)) != -1) {
sb.append(new String(buf, 0, len, "UTF-8"));
}
is.close();
jsonString = sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
return jsonString;
}
public List<LinkDataBean> mockLinkLineData(Context context, int index) {
if (context == null) {
return null;
}
List<LinkDataBean> mockResp = new ArrayList<>();
StringBuilder sb = new StringBuilder("linkline/data_json");
sb.append(index);
sb.append(".json");
if (!TextUtils.isEmpty(sb.toString())) {
String jsonString = getJsonFromAssets(context, sb.toString());
Type type = new TypeToken<ArrayList<LinkDataBean>>() {
}.getType();
try {
mockResp = new Gson().fromJson(jsonString, type);
} catch (JsonSyntaxException e) {
e.printStackTrace();
}
}
return mockResp;
}
}
XML布局
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:id="@+id/btn_0"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="文字连线题"
android:textAllCaps="false"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="图片连线题"
android:textAllCaps="false"
app:layout_constraintTop_toBottomOf="@+id/btn_0" />
<Button
android:id="@+id/btn_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="连线题——纯展示模式"
android:textAllCaps="false"
app:layout_constraintTop_toBottomOf="@+id/btn_1" />
</androidx.constraintlayout.widget.ConstraintLayout>
activity_link_line_text.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/fl_link_line"
android:layout_marginTop="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10.0dip"
android:background="#fff"
android:paddingLeft="13.0dip"
android:paddingRight="13.0dip"
android:paddingBottom="20.0dip"
app:layout_constraintTop_toTopOf="parent">
<com.tinytongtong.linklinedemo.LinkLineView
android:id="@+id/link_line_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</FrameLayout>
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
app:layout_constraintTop_toBottomOf="@+id/fl_link_line" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
activity_link_line_show_mode.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/fl_link_line"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="10.0dip"
android:background="#fff"
android:paddingLeft="13.0dip"
android:paddingRight="13.0dip"
android:paddingBottom="20.0dip"
app:layout_constraintTop_toTopOf="parent">
<com.tinytongtong.linklinedemo.LinkLineView
android:id="@+id/link_line_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</FrameLayout>
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
app:layout_constraintTop_toBottomOf="@+id/fl_link_line" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
activity_link_line_image.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/fl_link_line"
android:layout_marginTop="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10.0dip"
android:background="#fff"
android:paddingLeft="13.0dip"
android:paddingRight="13.0dip"
android:paddingBottom="20.0dip"
app:layout_constraintTop_toTopOf="parent">
<com.tinytongtong.linklinedemo.LinkLineView
android:id="@+id/link_line_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</FrameLayout>
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
app:layout_constraintTop_toBottomOf="@+id/fl_link_line" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
源码地址
https://github.com/tinyvampirepudge/LinkLineDemo