移动端h5 使用canvas 实现连线答题功能

1,496 阅读1分钟

1. 先看效果图

nc10u-5vab9.gif

2. 需求描述

移动端,有个能连线答题的功能。从左侧拖线移到右侧。

  1. 线需要在高度不固定的情况下,依旧生效,并且保持在中间的点。
  2. 只要求 1对1 的情况。

3. 解决思路

  1. 首先左右二侧的选项我就不说了。重点是划线部分。
  2. 借助于 canvas 实现 画线功能,查看示意图。
图例.png
  1. 二个canvas标签,一个是触摸中 划线的展示,另一个是为了触摸结束之后,真正展示的划线功能。

  2. 难点是,touchend事件并不会知晓我拖到哪个标签上了。这和 鼠标事件 截然不同。

  3. 思路:是不是可以手动算出来到哪的节点了?其实js提供了这个方法。document.elementFromPoint(event.pageX, event.pageY); 根据 x,y 得出当前的dom元素。

4. 代码展示

  1. 说那么多,我直接贴代码了。如果有不同的业务逻辑,可以自己修改调整。重点的划线和拖动功能,都已经写好了。
<template>
  <div class="connect" ref="connect" @touchmove.prevent>
    <div class="answer">
      <div class="answer-box">
        <div
          class="answer-box-item"
          v-for="(item, index) in leftArr"
          :key="item.label"
          ref="left"
          @touchstart="(e) => touchstart(e, item, index)"
          @touchend="(e) => touchend(e, item, index)"
          @touchmove="(e) => touchmove(e, item, index)"
        >
          {{ item.label }}
        </div>
      </div>
      <div class="answer-box">
        <div class="answer-box-item" v-for="item in rightArr" :key="item.label" ref="right">
          {{ item.label }}
        </div>
      </div>
    </div>
    <canvas class="connect-canvasA" :width="clientWidth" :height="clientHeight" ref="canvasA"></canvas>
    <canvas class="connect-canvasB" :width="clientWidth" :height="clientHeight" ref="canvasB"></canvas>
  </div>
</template>

<script>
export default {
  data() {
    return {
      leftArr: [],
      rightArr: [],
      location: [],
      canvasA: null,
      canvasB: null,
      leftDom: [],
      rightDom: [],
      clientWidth: 0,
      clientHeight: 0,
      scrollTop: 0,
      debounce: false
    };
  },
  props: {
    item: {
      type: Object,
      default: () => {
        return {
          left: [],
          right: []
        };
      }
    },
    value: {
      type: Array,
      default: () => {
        return [];
      }
    }
  },
  watch: {
    item: {
      immediate: true,
      handler(val) {
        if (val.left && val.left.length) this.init();
      }
    }
  },
  mounted() {
    // 添加滚动事件 监听 解决因为滚动引起的拖动线不对的问题
    window.addEventListener(
      'scroll',
      (e) => {
        // 加个防抖
        if (this.debounce) clearTimeout(this.debounce);
        this.debounce = setTimeout(() => {
          this.debounce = false;
          this.scrollTop = e.target.scrollTop;
        }, 500);
      },
      true
    );
    let connect = this.$refs.connect;
    this.clientWidth = connect.clientWidth;
    this.clientHeight = connect.clientHeight;
    this.canvasA = this.$refs.canvasA.getContext('2d');
    this.canvasB = this.$refs.canvasB.getContext('2d');
    this.$nextTick(() => {
      this.drawing();
    });
  },
  methods: {
    init() {
      this.leftArr = this.item.left.map((r) => {
        return {
          label: r,
          line: [],
          value: []
        };
      });
      this.rightArr = this.item.right.map((r) => {
        return {
          label: r
        };
      });
      // 等dom 渲染完成
      this.$nextTick(() => {
        this.leftDom = this.$refs.left.map((r, i) => {
          return {
            bom: r,
            index: i
          };
        });
        this.rightDom = this.$refs.right.map((r, i) => {
          return {
            bom: r,
            index: i
          };
        });
        this.value.map((r) => {
          this.leftArr[r.left].line = this.attachment(r.left, r.right);
          this.leftArr[r.left].value = [r.right];
        });
        this.drawing();
      });
    },
    // 触摸结束
    touchend(e, item, index) {
      let event = e.changedTouches[0];
      // document.elementFromPoint 重点,根据x,y坐标 取当前元素 所有能运行的逻辑 都依托于这里。
      let dom = document.elementFromPoint(event.pageX, event.pageY);
      // 右边的dom是哪个
      let right = this.rightDom.find((r) => r.bom === dom);
      // 不管是哪个都清除掉 底部的线
      this.canvasB.clearRect(0, 0, this.clientWidth, this.clientHeight);
      // 如果不是右边的dom 直接把 线 干掉 -- 证明不是 没有拖到右边上
      if (!right) {
        item.line = [];
        return;
      }
      // 如果已有的不是我自己 直接替换掉上一个的
      if (item.value[0] !== right.index) {
        let model = this.leftArr.find((r) => r.value[0] === right.index);
        if (model) {
          model.value = [];
          model.line = [];
        }
        item.value = [right.index];
      }
      // 重新赋值 线的 x y 轴
      item.line = this.attachment(index, right.index);
      console.log(item);
      this.drawing();
      let model = this.leftArr
        .map((r, i) => {
          return {
            left: i,
            right: r.value[0]
          };
        })
        .filter((r) => r.right !== undefined);
      this.$emit('input', model);
      console.log(JSON.stringify(model));
    },
    // 触摸开始
    touchstart(e, item) {
      let event = e.targetTouches[0];
      item.line = [event.pageX, event.pageY - this.$refs.connect.offsetTop + this.scrollTop];
    },
    // 触摸中
    touchmove(e, item) {
      let event = e.targetTouches[0];
      item.line[2] = event.pageX;
      item.line[3] = event.pageY - this.$refs.connect.offsetTop + this.scrollTop;
      this.backstrockline(item.line);
    },
    // 拖动的时候画线
    backstrockline(val) {
      let canvasB = this.canvasB;
      canvasB.clearRect(0, 0, this.clientWidth, this.clientHeight);
      canvasB.save();
      canvasB.beginPath();
      canvasB.lineWidth = 2;
      canvasB.moveTo(val[0], val[1]);
      canvasB.lineTo(val[2], val[3]);
      canvasB.strokeStyle = '#0C6';
      canvasB.stroke();
      canvasB.restore();
    },
    // 渲染出拖动之后的线
    drawing() {
      let canvasA = this.canvasA;
      this.canvasA.clearRect(0, 0, this.clientWidth, this.clientHeight);
      canvasA.beginPath();
      canvasA.lineWidth = 2;
      for (let i = 0; i < this.leftArr.length; i++) {
        const line = this.leftArr[i].line;
        if (line.length) {
          canvasA.moveTo(line[0], line[1]);
          canvasA.lineTo(line[2], line[3]);
        }
      }
      canvasA.strokeStyle = '#0C6';
      canvasA.stroke();
    },
    // 根据 左边 和右边的 index,换算出 左右的X,Y轴坐标
    attachment(index, rightIndex) {
      let leftEvent = this.leftDom[index].bom;
      let rightEvent = this.rightDom[rightIndex].bom;
      // 为了让线都在中间 x轴不用改 最简单
      let leftX = leftEvent.clientWidth + leftEvent.offsetLeft;
      let rightX = rightEvent.offsetLeft;
      let leftY = leftEvent.offsetTop + leftEvent.clientHeight / 2;
      let rightY = rightEvent.offsetTop + rightEvent.clientHeight / 2;
      return [leftX, leftY, rightX, rightY];
    }
  }
};
</script>

<style lang="scss" scoped>
.connect {
  position: relative;
  padding: 10px;
  &-canvasA {
    position: absolute;
    left: 0px;
    top: 0px;
    z-index: 1;
  }
  &-canvasB {
    position: absolute;
    left: 0px;
    top: 0px;
    z-index: 0;
  }
}
.answer {
  width: 100%;
  display: flex;
  justify-content: space-between;

  &-box {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    width: 30%;
    &-item {
      z-index: 2;
      display: inline-flex;
      padding: 10px;
      background-color: rgb(140, 240, 215);
      border-radius: 4px;
      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
      margin-bottom: 10px;
    }
    &-item:last-child {
      margin-bottom: 0;
    }
  }
}
</style>