Android实现连线题效果

news2024/10/1 3:32:53

效果图

全部正确:

有对有错:

结果展示,纯黑色:

支持图片:

实现思路

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

每个矩形的宽高都一样,或者等比例,这样利于给他们定位,添加矩形时使用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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/374780.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

并非从0开始的c++ day8

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

Delphi 中 FireDAC 数据库连接(定义连接)

一、定义连接&#xff08;FireDAC&#xff09;概述连接定义是一组参数&#xff0c;它定义了如何使用特定的FireDAC驱动将一个应用程序连接到DBMS。它相当于一个BDE别名、ADO UDL&#xff08;存储的OLEDB连接字符串&#xff09;或ODBC数据源名称&#xff08;DSN&#xff09;。关…

Vue下载安装步骤的详细教程(亲测有效) 1

目录 一、【准备工作】nodejs下载安装(npm环境) 1 下载安装nodejs 2 查看环境变量是否添加成功 3、验证是否安装成功 4、修改模块下载位置 &#xff08;1&#xff09;查看npm默认存放位置 &#xff08;2&#xff09;在 nodejs 安装目录下&#xff0c;创建 “node_global…

Spring中AOP的使用以及举例

1.AOP的简介 1.1 什么是AOP? AOP(Aspect Oriented Programming)面向切面编程&#xff0c;一种编程范式&#xff0c;指导开发者如何组织程序结构。 OOP(Object Oriented Programming)面向对象编程 我们都知道OOP是一种编程思想&#xff0c;那么AOP也是一种编程思想&#xf…

模块电源DC/DC直流隔离升压稳压HRB系列5v12v24v转50v100v110v200v220v250v300v400v

特点效率高达80%以上1*1英寸标准封装单电压输出稳压输出工作温度: -40℃~85℃阻燃封装&#xff0c;满足UL94-V0 要求温度特性好可直接焊在PCB 上应用HRB 0.2~10W 系列模块电源是一种DC-DC升压变换器。该模块电源的输入电压分为&#xff1a;4.5~9V、9~18V、及18~36VDC标准&#…

中小企业数字化自动化转型的方法

自动化是我们国内未来的趋势。智能制造的实现主要依托两个基础能力&#xff0c;一个是工业制造技术&#xff0c;另一个就是工业互联网。而自动化是工业制造技术的重要组成部分&#xff0c;是高度智能制造装备的核心部分&#xff0c;与承接着制造单元与工业互联网这两大核心。懂…

Polkadot 基础

Polkadot Polkadot联合并保护了一个不断增长的专业区块链生态系统&#xff0c;称为parachains。Polkadot上的应用程序和服务可以安全地跨链通信&#xff0c;形成真正可互操作的去中心化网络的基础。 真正的互操作性 Polkadot支持跨区块链传输任何类型的数据或资产&#xff0c;…

基于51单片机和proteus的智能调速风扇设计

此智能风扇是基于51单片机和proteus的仿真设计&#xff0c;功能如下&#xff1a; 1. Timer0 PWM控制电机转速 2. DHT11采集温湿度 3. LCD1602显示温湿度及电机状态 4. 按键控制电机加减速启停等 5. 串口控制电机加减速启停等 功能框图如下&#xff1a; Proteus仿真界面如下…

EasyExcel使用与详细说明,EasyExcel工具类

文章目录1.Apache POI1.1 学习使用成本较高1.2 POI的内存消耗较大1.3 特点2. 初识EasyExcel2.1 重写了POI对07版Excel的解析2.2 特点3.快速入门3.1 导入依赖坐标3.2 最简单的读3.2.1 需求、准备工作3.2.2 编写导出数据的实体3.2.3 读取Excel的监听器&#xff0c;用于处理读取产…

rabbitmq在linux系统下安装步骤

第一步&#xff1a;登录官网 官网地址&#xff1a;www.rabbitmq.com,点击Get Started 重要信息&#xff1a;RabbitMQ Tutorials手册&#xff0c;描述了工作模式 第二步&#xff1a;点击Download Installation下载 重要信息&#xff1a;rabbitmq是用erlang语言开发的&#xff0…

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

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

Vue-router 3.x 版本中路由守卫钩子函数解析

目录概念&#xff1a;分类全局前置守卫 &#xff08; router.beforeEach &#xff09;全局解析守卫 &#xff08; router.beforeResolve &#xff09;全局后置守卫 &#xff08; router.afterEach &#xff09;路由独享守卫 &#xff08; beforeEnter &#xff09;组件内的守卫…

有哪些办法可以降低 Redis 的内存使用情况

在逛知乎时&#xff0c;看到这样一个问题&#xff0c;觉得挺不错的&#xff0c;将自己个人的见解分享给大家。问题是:有哪些办法可以降低 Redis 的内存使用情况&#xff1f;个人也对Redis做了一个比较全面的问题汇总&#xff0c;希望对大家有所帮助。Redis面试题汇总要降低内存…

Git同时配置Github和Gitlab

电脑的git需要同时管理Github上自己的代码仓库和Gitlab的公司的代码仓库&#xff0c;所以记录同时配置两者的步骤。 第一步、清除已有的全局配置&#xff08;我之前只有github的配置&#xff09; git config --global --unset user.name git config --global --unset user.em…

【论文阅读总结】Mask R-CNN翻译总结

Mask R-CNN1.摘要Mask R-CNN相关介绍与优点2.引言3.文献综述3.1 R-CNN3.2 Instance Segmentation【实例分割】4. Mask R-CNN介绍4.1 Faster R-CNN(相关细节请看相关文章)4.2 Mask R-CNN4.3 Mask Representation【遮罩表示法】4.4 RoIAlign【感兴趣区域对齐】4.4.1 RoIPool【感兴…

软件测试面试问答

笔试 笔试的话我们需要揣测具体会考什么内容&#xff0c;我们可以通过招聘信息去了解该公司需要什么样的技能&#xff0c;以此来准备笔试。一般必考的内容会有编程&#xff0c;测试用例设计&#xff0c;工作流程&#xff0c;逻辑思维等内容&#xff0c;除此之外每个公司可能还会…

【设计模式】我终于读懂了模板方法模式。。。

&#x1f34e;豆浆制作问题 编写制作豆浆的程序&#xff0c;说明如下: 1.制作豆浆的流程 选材—>添加配料—>浸泡—>放到豆浆机打碎 2)通过添加不同的配料&#xff0c;可以制作出不同口味的豆浆 3)选材、浸泡和放到豆浆机打碎这几个步骤对于制作每种口味的豆浆都是一样…

二、IA-32系列处理器 通用寄存器介绍

IA-32系列处理器 通用寄存器介绍 寄存器处理器内的特殊存储单元处理器内有多种不同用途的寄存器寄存器分别有各自的名称,以便表示及访问通用寄存器IA-32系列CPU有8个32位的通用寄存器(General-Purpose Registers)通用寄存器不仅能存储数据,而且能参与算术逻辑运算,还能给出…

OpenCV 图像轮廓检测

本文是OpenCV图像视觉入门之路的第15篇文章&#xff0c;本文详细的介绍了图像轮廓检测的各种操作&#xff0c;例如&#xff1a;轮廓检索模式、轮廓逼近算子等操作。 图像轮廓是具有相同颜色或灰度的连续点的曲线&#xff0c;轮廓在形状分析和物体的检测和识别中很有用。图像轮廓…

预训练BERT

与PTB数据集相比&#xff0c;WikiText-2数据集保留了原来的标点符号、大小写和数字&#xff0c;并且比PTB数据集大了两倍多。 我们可以任意访问从WikiText-2语料库中的一对句子生成的预训练&#xff08;遮蔽语言模型和下一句预测&#xff09;样本。 原始的BERT有两个版本&…