当前位置: 首页 > news >正文

Android实现连线题效果

效果图

全部正确:

有对有错:

结果展示,纯黑色:

支持图片:

实现思路

仔细分析可以发现,连线题的布局可以分为两部分,一个是左右两列矩形,另一个是他们之间的连线。

每个矩形的宽高都一样,或者等比例,这样利于给他们定位,添加矩形时使用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方法@Overridepublic 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);}@Overridepublic 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;}@Overrideprotected 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);}@Overrideprotected 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);}@Overrideprotected 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);}@Overrideprotected 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;@Overrideprotected 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;}@Overridepublic 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;}@Overridepublic String toString() {return "LinkLineBean{" +"startX=" + startX +", startY=" + startY +", endX=" + endX +", endY=" + endY +", colorString='" + colorString + '\'' +", isRight=" + isRight +", leftIndex=" + leftIndex +", rightIndex=" + rightIndex +'}';}@Overridepublic 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);}@Overridepublic 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"><Buttonandroid: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" /><Buttonandroid: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" /><Buttonandroid: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.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><FrameLayoutandroid: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.LinkLineViewandroid:id="@+id/link_line_view"android:layout_width="fill_parent"android:layout_height="wrap_content" /></FrameLayout><TextViewandroid: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.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><FrameLayoutandroid: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.LinkLineViewandroid:id="@+id/link_line_view"android:layout_width="fill_parent"android:layout_height="wrap_content" /></FrameLayout><TextViewandroid: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.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><FrameLayoutandroid: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.LinkLineViewandroid:id="@+id/link_line_view"android:layout_width="fill_parent"android:layout_height="wrap_content" /></FrameLayout><TextViewandroid: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

相关文章:

Android实现连线题效果

效果图全部正确&#xff1a;有对有错&#xff1a;结果展示&#xff0c;纯黑色&#xff1a;支持图片&#xff1a;实现思路仔细分析可以发现&#xff0c;连线题的布局可以分为两部分&#xff0c;一个是左右两列矩形&#xff0c;另一个是他们之间的连线。每个矩形的宽高都一样&…...

以数据 见未来!首届未来数商大会成功举办

2月25日&#xff0c;2023未来数商大会在杭州未来科技城学术交流中心举办。大会发布了数商产业趋势研究报告&#xff0c;首次提出并探讨了完整的数商产业概念&#xff0c;并成立了未来数商联盟&#xff0c;开通了浙江大数据交易服务平台余杭专区。会上&#xff0c;杭州未来科技城…...

Java数据结构与算法——手撕LRULFU算法

LRU算法 力扣146&#xff1a;https://leetcode-cn.com/problems/lru-cache/ 讲解视频&#xff1a;https://www.bilibili.com/video/BV1Hy4y1B78T?p65&vd_source6f347f8ae76e7f507cf6d661537966e8 LRU是Least Recently Used的缩写&#xff0c;是一种常用的页面置换算法&…...

20230227英语学习

Can Clay Capture Carbon Dioxide? 低碳新思路&#xff1a;粘土也能吸收二氧化碳&#xff01; The atmospheric level of carbon dioxide — a gas that is great at trapping heat, contributing to climate change — is almost double what it was prior to the Industria…...

校招前端高频react面试题合集

了解redux吗&#xff1f; redux 是一个应用数据流框架&#xff0c;主要解决了组件之间状态共享问题&#xff0c;原理是集中式管理&#xff0c;主要有三个核心方法&#xff1a;action store reduce 工作流程 view 调用store的dispatch 接受action传入的store&#xff0c;reduce…...

k8s node之间是如何通信的?

承接上文同一个node中pod之间如何通信&#xff1f;单一Pod上的容器是怎么共享网络命名空间的&#xff1f;每个node上的pod ip和cni0网桥ip和flannel ip都是在同一个网段10.1.71.x上。cni0网桥会把报文发送flannel这个网络设备上&#xff0c;flannel其实是node上的一个后台进程&…...

System V|共享内存基本通信框架搭建|【超详细的代码解释和注释】

前言 那么这里博主先安利一下一些干货满满的专栏啦&#xff01; 手撕数据结构https://blog.csdn.net/yu_cblog/category_11490888.html?spm1001.2014.3001.5482这里包含了博主很多的数据结构学习上的总结&#xff0c;每一篇都是超级用心编写的&#xff0c;有兴趣的伙伴们都支…...

魔兽世界WoW注册网站搭建——-Liunx

问题背景哎 搭建了一个魔兽3.35&#xff08;纯洁版&#xff09;每当同学朋友要玩的时候我都直接worldserver上面打一个命令随之出现朋友的朋友也要玩想了想还是要有一个网站原本以为吧单机版里面网页的IP数据库改下可以了结果PHP报错了Unknown column sha_pass_hash in field l…...

OSG三维渲染引擎编程学习之六十八:“第六章:OSG场景工作机制” 之 “6.8 OSG内存管理”

目录 第六章 OSG场景工作机制 6.8 OSG内存管理 6.8.1 Referenced类 6.8.2 ref_ptr<>模板类 6.8.3 智能指针...

字节前端必会面试题(持续更新中)

事件传播机制&#xff08;事件流&#xff09; 冒泡和捕获 谈一谈HTTP数据传输 大概遇到的情况就分为定长数据 与 不定长数据的处理吧。 定长数据 对于定长的数据包而言&#xff0c;发送端在发送数据的过程中&#xff0c;需要设置Content-Length,来指明发送数据的长度。 当…...

内存数据库-4-[redis]在ubuntu中离线安装

Ubuntu20.04(linux)离线安装redis 官网redis下载地址 下载安装包redis-6.0.9.tar.gz。 1 下载安装 (1)解压 sudo tar -xzvf redis-6.0.9.tar.gz -C /usr/local/ cd /usr/local/redis-6.0.9/(2)编译 sudo make(3)测试 sudo dpkg -i libtcl8.6_8.6.10dfsg-1_amd64.deb sudo d…...

并非从0开始的c++ day8

并非从0开始的c day8结构体结构体嵌套二级指针练习结构体偏移量内存对齐内存对齐的原因如何内存对齐文件操作文件的概念流的概念文本流二进制流文件缓冲区文件打开关闭文件关闭fclose文件读写函数回顾按格式化读写文件文件读写注意事项结构体 结构体嵌套二级指针练习 需求&am…...

ubuntu下用i686-w64-mingw32交叉编译支持SDL、Openssl的ffmpeg库

前言 本篇博客是基于前两篇关于ffmpeg交叉编译下&#xff0c;进行再次编译操作。ubuntu下ffmpeg的交叉编译环境搭建可以参看以下我的这篇博客&#xff1a;https://blog.csdn.net/linyibin_123/article/details/108759367 &#xff1b; ubuntu下交叉编译openssl及交叉编译支持o…...

对IDEA中断点Suspend 属性理解

suspend的类型分为 1、ALL&#xff1a;有线程进入该断点时&#xff0c;暂停所有线程 2、Thread&#xff1a;有线程进入该断点时&#xff0c;只暂停该线程 讨论下不同线程在同一时间段都遇到断点时&#xff0c;idea的处理方法。假如在执行时间上&#xff0c;thread1会先进入断…...

IM即时通讯开发如何解决大量离线消息导致客户端卡顿的

大部分做后端开发的朋友&#xff0c;都在开发接口。客户端或浏览器h5通过HTTP请求到我们后端的Controller接口&#xff0c;后端查数据库等返回JSON给客户端。大家都知道&#xff0c;HTTP协议有短连接、无状态、三次握手四次挥手等特点。而像游戏、实时通信等业务反而很不适合用…...

【软件测试】测试老鸟的迷途,进军高级自动化测试测试......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 很多从业几年的选手…...

HMM(隐马尔科夫模型)-理论补充2

目录 一.大数定理 二.监督学习方法 1.初始概率 2.转移概率 3.观测概率 三.Baum-Welch算法 1.EM算法整体框架 2. Baum-Welch算法 3.EM过程 4.极大化 5.初始状态概率 6.转移概率和观测概率 四.预测算法 1.预测的近似算法 2.Viterbi算法 1.定义 2. 递推&#xff1…...

【分布式系统】MinIO之Multi-Node Multi-Drive架构分析

文章目录架构分析节点资源硬盘资源服务安装安装步骤创建系统服务新建用户和用户组创建环境变量启动服务负载均衡代码集成注意最近打算使用MinIO替代原来使用的FastDFS&#xff0c;所以一直在学习MinIO的知识。这篇文章是基于MinIO多节点多驱动的部署进行研究。 架构分析 节点资…...

【无标题】(2019)NOC编程猫创新编程复赛小学组真题含参考

&#xff08;2019&#xff09;NOC编程猫创新编程复赛小学组最后6道大题。前10道是选择填空题 略。 这道题是绘图题&#xff0c;没什么难度&#xff0c;大家绘制这2个正十边形要注意&#xff1a;一是不要超出舞台&#xff1b;二是这2个正十边形不要相交。 这里就不给出具体程序了…...

【尚硅谷MySQL入门到高级-宋红康】数据库概述

1、为什么要使用数据库 数据的持久化 2、数据库与数据库管理系统 2.1 数据库的相关概念 2.2 数据库与数据库管理系统的关系 3、 MySQL介绍 MySQL从5.7版本直接跳跃发布了8.0版本 &#xff0c;可见这是一个令人兴奋的里程碑版本。MySQL 8版本在功能上做了显著的改进与增强&a…...

SpringBoot集成Redis并实现数据缓存

应用场景 存放Token、存放用户信息或字典等需要频繁访问数据库获取但不希望频繁访问增加数据库压力且变化不频繁的数据。 集成步骤 1. 新建 Maven 项目并引入 redis 依赖【部分框架有可能已经集成&#xff0c;会导致依赖文件有差异】 <dependency><groupId>org…...

SpringBoot配置文件(properties yml)

查看官网更多系统配置项&#xff1a;https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties 1.配置⽂件作⽤ 整个项⽬中所有重要的数据都是在配置⽂件中配置的&#xff0c;⽐如&#xff1a;数据库的连接信息&am…...

css 画图之质感盒子

前言 css 众所周知可以做很多的事情&#xff0c;比如&#xff1a;界面效果、特效、独特的样式等。今天给各位朋友带来的是以box-shadow来画一个很有质感效果的一个盒子。 之前在网上冲浪的时候&#xff0c;发现了这样的一个效果&#xff0c;所以来记录一下。 下面是实现后的…...

面了一个月,终于让我总结出了这份最详细的接口测试面试题

目录 1、你们公司是如何做接口测试的&#xff1f; 2、什么时候开展接⼝测试&#xff1f; 3、接⼝测试和UI测试的工作是否重复&#xff1f; 4、接口测试框架怎么搭建&#xff1f; 5、接⼝之间有依赖时怎么处理&#xff1f; 6、如何判断接⼝测试的结果&#xff08;成功或失败&a…...

{新}【java开发环境安装】完整工作环境安装配置

公司新发了一台红米笔记本&#xff0c;打算用新的笔记本&#xff0c;开启自己新的工作旅程&#xff0c;其中把做个的事都记录一边&#xff0c;以便实现&#xff0c;听、读、视频图像、讨论、实践、教人的一个学习过程。 一、Java开发环境安装 找到安装包下载&#xff1b;在官…...

Python|每日一练|数组|数学|图算法|字符串|动态规划|单选记录:加一|迷宫问题|扰乱字符串

1、加一&#xff08;数组&#xff0c;数学&#xff09; 给定一个由 整数 组成的 非空 数组所表示的非负整数&#xff0c;在该数的基础上加一。 最高位数字存放在数组的首位&#xff0c; 数组中每个元素只存储单个数字。 你可以假设除了整数 0 之外&#xff0c;这个整数不会以…...

MySQL 使用IF判断

mysql判断语句 1、IF 和IFNULL IF(表达式1&#xff0c;表达式2&#xff0c;表达式3); 含义&#xff1a;如果表达式1为true&#xff0c;则返回表达式2的值&#xff0c;否则返回表达式3的值&#xff0c;表达式的值类型可以为数字或字符串 例&#xff1a;判断对错 SELECT IF(TRUE…...

C++类与对象(上)【详析】

目录1.面向过程和面向对象初步认识2.类的引入3.类的定义4.类的访问限定符及封装4.1访问限定符4.2封装5.类的作用域6.类的实例化7.类对象模型7.1 如何计算类对象的大小8.this关键字如果说我们对C的初步认识&#xff0c;是觉得C是对C语言不足之处的进行修补&#xff0c;在认识完类…...

AIR系列|板载LED|gpio引脚选择|GPIO|流水灯|LuatOS-SOC接口|官方demo|学习(20-1):GPIO库基础

AIR系列各型号开发板板载LED对应管脚及GPIO控制代码 AIR103&#xff1a; rtos_bsp "AIR103" then -- Air103开发板LED引脚编号--return pin.PB26, pin.PB25, pin.PB24return 42,41,40 AIR105&#xff1a; rtos_bsp "AIR105" then -- Air105开发板LED引…...

MySQL数据库中的函数怎样使用?

函数 是指一段可以直接被另一段程序调用的程序或代码。 也就意味着&#xff0c;这一段程序或代码在MySQL中已经给我们提供了&#xff0c;我们要做的就是在合适的业务场景调用对应的函数完成对应的业务需求即可。 那么&#xff0c;函数到底在哪儿使用呢?我们先来看两个场景&…...

专做水果的网站/网站空间

2019独角兽企业重金招聘Python工程师标准>>> 两种方式判断访问终端是否是微信浏览器 JS判断 function is_weixin() { var ua window.navigator.userAgent.toLowerCase(); if (ua.match(/MicroMessenger/i) micromessenger) { $("#rs").text("微信…...

涉密项目单位网站建设流程/手机网页制作软件

一、设置动态获取IP dhclient命令使用动态主机配置协议动态的配置网络接口的网络参数。 语法 dhclient(选项)(参数) 选项 0&#xff1a; 指定dhcp客户端监听的端口号&#xff1b; -d&#xff1a;总是以前台方式运行程序&#xff1b;-q&#xff1a;安静模式&#xff0c;不打印…...

上海建设单位工程备案网站/hao123网址导航

从Theano到Lasagne&#xff1a;基于Python的深度学习的框架和库 摘要&#xff1a;最近&#xff0c;深度神经网络以“Deep Dreams”形式在网站中如雨后春笋般出现&#xff0c;或是像谷歌研究原创论文中描述的那样&#xff1a;Inceptionism。在这篇文章中&#xff0c;我们将讨论几…...

建设网站一定要会代码吗/关键词是网站seo的核心工作

客服微信&#xff1a;meijing8001您只管发布&#xff0c;我们来为您宣传你好香河、香河新鲜事、香河招聘网指尖香河、香河限号、香河生活通等无论您在哪里发布&#xff0c;这些平台都将同步显示从此找工作&#xff0c;招人才就是这么简单&#xff01;2020年10月31日统计全新打造…...

个人网站的设计和建设/北京seo网站设计

1.对象创建型模式 1.3 Abstract Factory模式 1.3.1 需求 在下面情况能够使用Abstract Factory模式: • 一个系统要独立于它的产品的创建、组合和表示时(这个需求和FactoryMethod类似)。 • 一个系统要由多个产品系列中的一个来配置时(这个需求也和Factory Method类…...

网站验证码 出不来/网络营销软件大全

作者简介&#xff1a;成锁元&#xff0c;Westar实验室技术专家。Westar实验室&#xff08;westar.io&#xff09;&#xff0c;成立于 2018 年&#xff0c;关注于区块链及分布式前沿技术&#xff0c;包括区块链分层架构、二层路由&#xff0c;网络性能、智能合约、PoW 优化等。 …...