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方法

    @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
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
要在 Android实现绘图加点连线,可以使用 Canvas 和 Paint 类。下面是一个简单的实现步骤: 1. 在 xml 布局文件添加一个自定义 View,用于绘制图形。 ```xml <com.example.drawview.DrawView android:id="@+id/draw_view" android:layout_width="match_parent" android:layout_height="match_parent" /> ``` 2. 在自定义 View 重写 onDraw 方法,在其绘制点和线。 ```java public class DrawView extends View { private Paint paint; private List<Point> points = new ArrayList<>(); private List<Pair<Point, Point>> lines = new ArrayList<>(); public DrawView(Context context, AttributeSet attrs) { super(context, attrs); paint = new Paint(); paint.setColor(Color.BLACK); paint.setStrokeWidth(5f); paint.setStyle(Paint.Style.FILL); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (Point point : points) { canvas.drawCircle(point.x, point.y, 10f, paint); } for (Pair<Point, Point> line : lines) { canvas.drawLine(line.first.x, line.first.y, line.second.x, line.second.y, paint); } } public void addPoint(Point point) { points.add(point); invalidate(); } public void addLine(Point start, Point end) { lines.add(new Pair<>(start, end)); invalidate(); } public void clear() { points.clear(); lines.clear(); invalidate(); } } ``` 3. 在 Activity 调用自定义 View 的方法,添加点和线。 ```java public class MainActivity extends AppCompatActivity { private DrawView drawView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); drawView = findViewById(R.id.draw_view); // 添加点和线 drawView.addPoint(new Point(100, 100)); drawView.addPoint(new Point(200, 200)); drawView.addLine(new Point(100, 100), new Point(200, 200)); } } ``` 这样就可以在 Android实现绘图加点连线了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

金戈鐡馬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值