| <!-- |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| --> |
| <!-- Created by zweipix on 2017-05-09. --> |
| <!-- Updated by zweipix on 2018-03-07. --> |
| |
| <template> |
| <div |
| ref="bar-container" |
| class="slider-bar-container" |
| :style="containerStyle"> |
| <div |
| class="range-bar" |
| :style="rangeBarStyle"> |
| <div |
| ref="value-bar" |
| class="value-bar" |
| :style="valueBarStyle"> |
| <div></div> |
| </div> |
| </div> |
| <div |
| ref="slide-block-1" |
| class="slide-block" |
| @panstart="webStartHandler" |
| @panmove="webMoveHandler1" |
| @panend="webEndHandler" |
| @touchstart="weexStartHandler1" |
| @touchend="weexEndHandler" |
| @horizontalpan="weexHandler1" |
| :prevent-move-event="preventMoveEvent" |
| :style="blockStyle1"> |
| <div></div> |
| </div> |
| <div |
| v-if="range" |
| ref="slide-block-2" |
| class="slide-block" |
| @panstart="webStartHandler" |
| @panmove="webMoveHandler2" |
| @panend="webEndHandler" |
| @touchstart="weexStartHandler2" |
| @touchend="weexEndHandler" |
| @horizontalpan="weexHandler2" |
| :prevent-move-event="preventMoveEvent" |
| :style="blockStyle2"> |
| <div></div> |
| </div> |
| </div> |
| </template> |
| |
| <script> |
| import Utils from '../utils'; |
| import BindEnv from '../utils/bind-env'; |
| import Binding from 'weex-bindingx/lib/index.weex.js'; |
| |
| const animation = weex.requireModule('animation'); |
| const dom = weex.requireModule('dom'); |
| |
| export default { |
| data: () => ({ |
| env: 'weex', |
| diffX1: 0, |
| diffX2: 0, |
| barWidth: 0, |
| preventMoveEvent: true, |
| minDist: 0, |
| // selectRange: [0, 0], |
| blockRadius: 28, |
| DPR: 1, |
| timeout: 100, |
| isAndroid: Utils.env.isAndroid() |
| }), |
| props: { |
| selectRange: { |
| type: Array, |
| default: [0, 0] |
| }, |
| length: { |
| type: Number, |
| default: 500 |
| }, |
| height: { |
| type: Number, |
| default: 4 |
| }, |
| // 是否双滑块模式 |
| range: { |
| type: Boolean, |
| default: false |
| }, |
| // 最小值 |
| min: { |
| type: Number, |
| default: 0 |
| }, |
| // 最大值 |
| max: { |
| type: Number, |
| default: 100 |
| }, |
| // 最小取值范围,用于范围选择范围最小差值 |
| minDiff: { |
| type: Number, |
| default: 5 |
| }, |
| // 设置当前取值。当 range 为 false 时,使用 number,否则用 [number, number] |
| value: { |
| type: [Number, Array], |
| default: 0 |
| }, |
| // 设置初始取值。当 range 为 false 时,使用 number,否则用 [number, number] |
| defaultValue: { |
| type: [Number, Array], |
| default: 0 |
| }, |
| // 值为 true 时,滑块为禁用状态 |
| disabled: { |
| type: Boolean, |
| default: false |
| }, |
| invalidColor: { |
| type: String, |
| default: '#E0E0E0' |
| }, |
| validColor: { |
| type: String, |
| default: '#EE9900' |
| }, |
| disabledColor: { |
| type: String, |
| default: '#AAA' |
| }, |
| blockColor: { |
| type: String, |
| default: '#ffffff' |
| } |
| }, |
| watch: { |
| value(e) { |
| if (!this.range) { |
| this.diffX1 = this._getDiffX(e || this.defaultValue); |
| } else { |
| this.diffX1 = this._getDiffX(e[0] || this.defaultValue[0]); |
| this.diffX2 = this._getDiffX(e[1] || this.defaultValue[1]); |
| this.barWidth = this.diffX2 - this.diffX1; |
| } |
| } |
| }, |
| created() { |
| if (Utils.env.isWeb()) { |
| this.env = 'web'; |
| this.DPR = window.devicePixelRatio ? window.devicePixelRatio : 1; |
| } else { |
| this.DPR = weex.config.env.scale; |
| } |
| }, |
| mounted() { |
| this.block1 = this.$refs['slide-block-1']; // 左侧滑块 |
| this.block2 = this.$refs['slide-block-2']; // 右侧滑块 |
| this.valueBar = this.$refs['value-bar']; // 黄色值条 |
| this.barContainer = this.$refs['bar-container']; // 滚动条容器 |
| |
| if (!this.range) { |
| this.diffX1 = this._getDiffX(this.value || this.defaultValue); |
| } else { |
| this.diffX1 = this._getDiffX(this.value[0] || this.defaultValue[0]); |
| this.diffX2 = this._getDiffX(this.value[1] || this.defaultValue[1]); |
| this.barWidth = this.diffX2 - this.diffX1; |
| } |
| |
| // 是否支持expresstionBinding |
| if (BindEnv.supportsEB() && Binding.prepare) { |
| this.block1 && Binding.prepare({ |
| anchor: this.block1.ref, |
| eventType: 'pan' |
| }); |
| this.block2 && Binding.prepare({ |
| anchor: this.block2.ref, |
| eventType: 'pan' |
| }); |
| this.valueBar && Binding.prepare({ |
| anchor: this.valueBar.ref, |
| eventType: 'pan' |
| }); |
| } |
| |
| if (this.range) { |
| this.selectRange = this.value || this.defaultValue; // 初始化范围选择返回数据 |
| this.minDist = (this.minDiff / (this.max - this.min)) * this.length; // 滑块1、2之前最小间距 |
| } |
| |
| // 由于weex在mounted后渲染是异步的不能确保元素渲染完成,需要异步执行 |
| setTimeout(() => { |
| dom.getComponentRect(this.barContainer, option => { |
| const { left } = option.size; |
| this.leftDiffX = left; |
| }); |
| }, 100); |
| }, |
| |
| computed: { |
| containerStyle() { |
| return { |
| width: `${this.length + 56}px`, |
| height: '56px' |
| }; |
| }, |
| rangeBarStyle() { |
| return { |
| width: `${this.length}px`, |
| height: `${this.height}px`, |
| flexDirection: 'row', |
| backgroundColor: this.invalidColor |
| }; |
| }, |
| valueBarStyle() { |
| let left = 0; |
| let width = 0; |
| |
| if (!this.range) { |
| left = this.diffX1 - this.length; |
| width = this.length; |
| } else { |
| left = this.diffX1; |
| width = this.diffX2 - this.diffX1; |
| } |
| |
| return { |
| width: width + 'px', |
| height: this.height + 'px', |
| transform: `translateX(${left}px)`, |
| backgroundColor: this.disabled ? this.disabledColor : this.validColor |
| }; |
| }, |
| blockStyle1() { |
| let left = this.diffX1; |
| return { |
| transform: `translateX(${left}px)`, |
| backgroundColor: this.blockColor |
| }; |
| }, |
| blockStyle2() { |
| return { |
| transform: `translateX(${this.diffX2}px)`, |
| backgroundColor: this.blockColor |
| } |
| } |
| }, |
| methods: { |
| getBlock1Value(callback) { |
| dom.getComponentRect(this.block1, option => { |
| const { left } = option.size; |
| const value = this._getValue(left - this.leftDiffX); |
| if (!this.range) { |
| callback && callback(value) |
| } else { |
| this.selectRange[0] = value; |
| callback && callback(this.selectRange) |
| } |
| }); |
| }, |
| getBlock2Value(callback) { |
| dom.getComponentRect(this.block2, option => { |
| const { left } = option.size; |
| this.selectRange[1] = this._getValue(left - this.leftDiffX); |
| callback && callback(this.selectRange) |
| }); |
| }, |
| // 更新单选值或最小值 |
| weexHandler1(e) { |
| const self = this; |
| switch (e.state) { |
| case 'start': |
| self.bindBlock1(); |
| break; |
| case 'move': |
| this.getBlock1Value((value) => this.$emit('updateValue', value)) |
| break; |
| case 'end': |
| this.getBlock1Value((value) => this.$emit('wxcSliderBarTouchEnd', value)) |
| break; |
| default: |
| break; |
| } |
| }, |
| |
| // 更新最大值 |
| weexHandler2(e) { |
| const self = this; |
| |
| switch (e.state) { |
| case 'start': |
| self.bindBlock2(); |
| break; |
| case 'move': |
| self.getBlock2Value(value => this.$emit('updateValue', value)); |
| break; |
| case 'end': |
| self.getBlock2Value(value => this.$emit('wxcSliderBarTouchEnd', value)); |
| break; |
| default: |
| break; |
| } |
| }, |
| |
| weexStartHandler1() { |
| // 由于android端不支持 horizontalpan 的move事件,使用setInterval hack方案 |
| if (!this.isAndroid) { |
| return; |
| } |
| this.firstInterval = setInterval(() => { |
| this.getBlock1Value(value => this.$emit('updateValue', value)) |
| }, this.timeout); |
| }, |
| |
| weexStartHandler2() { |
| if (!this.isAndroid) { |
| return; |
| } |
| // 由于android端不支持 horizontalpan 的move事件,使用setInterval hack方案 |
| this.secondInterval = setInterval(() => { |
| this.getBlock2Value((value) => this.$emit('updateValue', value)) |
| }, this.timeout); |
| }, |
| |
| // 清除定时器 |
| weexEndHandler() { |
| if (!this.isAndroid) { |
| return; |
| } |
| this.firstInterval && clearInterval(this.firstInterval); |
| this.secondInterval && clearInterval(this.secondInterval); |
| if (this.range) { |
| this.getBlock2Value(value => this.$emit('updateValue', value)) |
| } else { |
| this.getBlock1Value(value => this.$emit('updateValue', value)) |
| } |
| }, |
| |
| webStartHandler(e) { |
| if (this.env === 'weex') { |
| return; |
| } |
| this.startX = e.touch.clientX; |
| this.startDiffX1 = this.diffX1; |
| this.startDiffX2 = this.diffX2; |
| }, |
| |
| webMoveHandler1(e) { |
| if (this.env === 'weex' || this.disabled) { |
| return; |
| } |
| |
| const deltaX = (e.touch.clientX - this.startX) * this.DPR; |
| const diff = this.startDiffX1 + deltaX; |
| |
| let max = this.length; |
| if (this.range) { |
| max = this.diffX2 - this.minDist; |
| } |
| |
| if (diff > 0 && diff < max) { |
| this.diffX1 = diff; |
| animation.transition(this.block1, { |
| styles: { |
| transform: `translateX(${this.diffX1}px)` |
| } |
| }, () => {}); |
| if (!this.range) { |
| this.$emit('updateValue', this._getValue(this.diffX1)); |
| } else { |
| this.selectRange[0] = this._getValue(this.diffX1); |
| this.$emit('updateValue', this.selectRange); |
| } |
| } |
| }, |
| |
| webEndHandler(e) { |
| if (this.env === 'weex' || this.disabled) { |
| return; |
| } |
| if (!this.range) { |
| this.$emit('wxcSliderBarTouchEnd', this._getValue(this.diffX1)); |
| } else { |
| this.selectRange[1] = this._getValue(this.diffX2); |
| this.$emit('wxcSliderBarTouchEnd', this.selectRange); |
| } |
| }, |
| webMoveHandler2(e) { |
| if (this.env === 'weex' || this.disabled) { |
| return; |
| } |
| |
| const deltaX = (e.touch.clientX - this.startX) * this.DPR; |
| const diff = this.startDiffX2 + deltaX; |
| const min = this.diffX1 + this.minDist; |
| const max = this.length; |
| |
| if (diff > min && diff < max) { |
| this.diffX2 = diff; |
| animation.transition(this.block2, { |
| styles: { |
| transform: `translateX(${this.diffX2}px)` |
| } |
| }, () => {}); |
| if (!this.range) { |
| this.$emit('updateValue', this._getValue(this.diffX2)); |
| } else { |
| this.selectRange[1] = this._getValue(this.diffX2); |
| this.$emit('updateValue', this.selectRange); |
| } |
| } |
| }, |
| bindBlock1() { |
| const self = this; |
| |
| // 如果禁用,不行进行表达式绑定 |
| if (self.disabled) { |
| Binding.unbind({ |
| token: this.gesToken1, |
| eventType: 'pan', |
| }) |
| this.gesToken1 = 0; |
| return; |
| } |
| |
| // 初始化按钮&条的大小范围 |
| let blockMax1 = 0; |
| if (self.range) { |
| blockMax1 = self.diffX2 - self.minDist; |
| } else { |
| blockMax1 = self.length; |
| } |
| |
| const barMax1 = self.diffX2; |
| |
| if (!self.range) { |
| |
| const startLeft = self.diffX1 - blockMax1 - self.minDist; |
| |
| const props = [{ |
| element: self.block1.ref, |
| property: 'transform.translateX', |
| expression: `min(${blockMax1}, max(x + ${self.diffX1}, 0))` |
| }, { |
| element: self.valueBar.ref, |
| property: 'transform.translateX', |
| expression: `min(0, max(x + ${startLeft}, -${blockMax1}))` |
| }]; |
| |
| const gesTokenObj = Binding.bind({ |
| anchor: self.block1.ref, |
| eventType: 'pan', |
| props |
| }, (e) => { |
| if (e.state === 'end' || e.state === 'cancel' || e.state === 'exit') { |
| const range = self.getRange(); |
| // 限制diffX1范围 |
| self.diffX1 = self._restrictValue(range.rangeX1, self.diffX1 + e.deltaX); |
| self.bindBlock1(); |
| } |
| }); |
| this.gesToken1 = gesTokenObj.token; |
| } else { |
| |
| // 选范围 |
| const props = [{ |
| element: self.block1.ref, |
| property: 'transform.translateX', |
| expression: `min(${blockMax1}, max(x + ${self.diffX1}, 0))` |
| }, { |
| element: self.valueBar.ref, |
| property: 'transform.translateX', |
| expression: `min(${blockMax1}, max(x + ${self.diffX1}, 0))` |
| }, { |
| element: self.valueBar.ref, |
| property: 'width', |
| expression: `min(${barMax1}, max(0, ${self.barWidth} - x))` |
| }]; |
| |
| const gesTokenObj = Binding.bind({ |
| anchor: self.block1.ref, |
| eventType: 'pan', |
| props |
| }, (e) => { |
| if (e.state === 'end' || e.state === 'cancel' || e.state === 'exit') { |
| const range = self.getRange(); |
| self.barWidth = self._restrictValue(range.rangeX1, self.barWidth - e.deltaX); |
| self.diffX1 = self._restrictValue(range.rangeX1, self.diffX1 + e.deltaX); |
| self.bindBlock1(); |
| } |
| }); |
| this.gesToken1 = gesTokenObj.token; |
| } |
| }, |
| |
| bindBlock2() { |
| const self = this; |
| |
| // 如果禁用,不行进行表达式绑定 |
| if (self.disabled) { |
| Binding.unbind({ |
| token: this.gesToken2, |
| eventType: 'pan', |
| }); |
| this.gesToken2 = 0; |
| return; |
| } |
| |
| // 初始化按钮&条的大小范围 |
| let blockMax1 = 0; |
| if (self.range) { |
| blockMax1 = self.diffX2 - self.minDist; |
| } else { |
| blockMax1 = self.length; |
| } |
| |
| const blockMax2 = self.length; |
| const blockMin2 = self.diffX1 + self.minDist; |
| const barMax2 = self.length - self.diffX1; |
| |
| const props = [{ |
| element: self.block2.ref, |
| property: 'transform.translateX', |
| expression: `min(${blockMax2}, max(x + ${self.diffX2}, ${blockMin2}))` |
| }, { |
| element: self.valueBar.ref, |
| property: 'width', |
| expression: `min(${barMax2}, max(0, x + ${self.barWidth}))` |
| }]; |
| |
| const gesTokenObj = Binding.bind({ |
| anchor: self.block2.ref, |
| eventType: 'pan', |
| props |
| }, (e) => { |
| if (e.state === 'end' || e.state === 'cancel' || e.state === 'exit') { |
| const range = self.getRange(); |
| self.barWidth = self._restrictValue([0, self.length - self.diffX1], self.barWidth + e.deltaX); |
| self.diffX2 = self._restrictValue(range.rangeX2, self.diffX2 + e.deltaX); |
| self.bindBlock2(); |
| } |
| }); |
| this.gesToken2 = gesTokenObj.token; |
| }, |
| |
| // 获取diffx1 diffx2 取值范围 |
| getRange() { |
| if (!this.range) { |
| return { |
| rangeX1: [0, this.length] |
| } |
| } else { |
| return { |
| rangeX1: [0, this.diffX2 - this.minDist], |
| rangeX2: [this.diffX1 + this.minDist, this.length] |
| } |
| } |
| }, |
| |
| // 限制取值范围 |
| _restrictValue(range, value) { |
| if (range && range.length && range.length === 2) { |
| if (value < range[0]) { |
| return range[0]; |
| } else if (value > range[1]) { |
| return range[1]; |
| } else { |
| return value; |
| } |
| } |
| }, |
| |
| // 根据x方向偏移量计算value |
| _getValue(diffX) { |
| return Math.round((diffX / this.length) * (this.max - this.min) + this.min); |
| }, |
| |
| // 根据value和length计算x方向偏移值 |
| _getDiffX(value) { |
| return ((value - this.min) / (this.max - this.min)) * this.length; |
| } |
| } |
| } |
| </script> |
| |
| <style scoped> |
| .slider-bar-container { |
| height: 56px; |
| justify-content: center; |
| align-items: center; |
| overflow: hidden; |
| } |
| |
| |
| .range-bar { |
| overflow: hidden; |
| } |
| |
| .value-bar { |
| height: 4px; |
| overflow: hidden; |
| } |
| |
| .slide-block { |
| width: 56px; |
| height: 56px; |
| border-radius: 28px; |
| border-width: 1px; |
| border-color: rgba(0, 0, 0, 0.1); |
| position: absolute; |
| left: 0; |
| bottom: 0; |
| } |
| </style> |