李毅超

李毅超 查看完整档案

南昌编辑  |  填写毕业院校上海钢联  |  前端 编辑 github.com/lycHub 编辑
编辑

主angular,兼ts|webpack|rxjs|vue,擅长造轮子

个人动态

李毅超 发布了文章 · 10月7日

angular_教程_angular 10_视频教程

各位同学大家好,2020 angular10语法 + 实战课已完成

很荣幸有许多朋友支持我2019年的ng8教程
(‾◡◝)

这次的教程将会把我所知道的ng语法全部教给大家,外加一个精心准备的实战,
带你领略angular的编程艺术
试看地址:Angular完全解读

查看原文

赞 1 收藏 1 评论 0

李毅超 收藏了文章 · 2019-10-17

原生js实现移动端选择器插件

原生js实现移动端选择器插件

前言

插件功能只满足我司业务需求,如果希望有更多功能的,可在下方留言,我尽量扩展!如果你有需要或者喜欢的话,可以给我github来个star ?

仓库地址

在线预览(记得将浏览器切换到手机模式)

预览

省市区

准备

首先在页面中引入css,js文件

每次需要弹出该组件时通过new一个实例来生成,代码如下:

var data = {
    1:{
      2:[3,4]
    }
}
var pickerView = new PickerView({
    bindElem: elem, // 绑定的元素,用于区别多个组件存在时回显区别,如果单个可以随意填某个元素
    data: data, // 说明:该参数必须符合json格式 且最里层是个数组,如上面的data变量所展示的[3,4]。
    title: '标题2', // 顶部标题文本 默认为“标题”
    leftText: '取消', // 头部左侧按钮文本 默认为‘取消’
    rightText: '确定', // 头部右侧按钮文本 默认为“确定”
    rightFn: function( selectArr ){  // 点击头部右侧按钮的回调函数,参数为一个数组,数组对应滚轮中每项对应的值

    }
});
字段介绍如上注释,滚轮的数量取决于你json嵌套的层数。如下:
var data = [1,2,3]

data1

var data = {
    "小明家":["小明爸爸","小明妈妈","小明爷爷","小明奶奶","小明爸爸","小明妈妈","小明爷爷","小明奶奶"],
    "小红家":["小红爸爸","小红妈妈"]
}

data2

案例

<!-- html -->
<button style="font-size:50px;" id="btn">按钮</button>
<div class="showText"></div>
button标签是用来每次点击的时候打开组件

div标签用来展示选择的内容

//js

// var data = 地级市json数据,过大 就不展示了

var data = {
    "小明家":{
      "小明爸爸":[1,2,6,7,7,8,8,9,0,6,98,76,5],
      "小明妈妈":[3,4]
    },
    "小红家":{
      "小红爸爸":[5,6],
      "小红妈妈":[7,8]
    }
}
var btn = document.getElementById("btn");
btn.onclick = function(){
  var pickerView = new PickerView({
      bindElem: btn,
      data: data,
      title: '家庭',
      leftText: '取消',
      rightText: '确定',
      rightFn: function( selectArr ){
          console.log(selectArr,'selectarr');
          // 将家庭成员展示到showText类名的div中
          document.querySelector(".showText").innerText = selectArr.join("-");
      }
  });
}
说明: 每次显示组件的时候都需要new一个实例,如上button标签每次被点击的时候都new一个。效果如下:

预览

结尾

如有什么功能需要增加的,可在评论区留言,我尽量满足。如有什么疏忽或错误,希望您指出。我会尽早修改,以免误导他人。

查看原文

李毅超 收藏了文章 · 2019-10-17

实现选择器(picker)插件

一个正常的选择器插件是非常细致的,一步一步来描述就是。手指滑动内容跟随手指滚动,当内容到底或触顶的时候就不能在滚动并且内容要一直保持在正确的位置上。

第一步分析插件结构

首先要有一个插件容器,整个插件容器包含渐变背景,选中实线,内容容器。效果类似于下面:

picker图片

所以对应的代码如下:

<div class="scroller-component" data-role="component">
    <div class="scroller-mask" data-role="mask"></div>
    <div class="scroller-indicator" data-role="indicator"></div>
    <div class="scroller-content" data-role="content">
        <div class="scroller-item" data-value='1'>1</div>
        <div class="scroller-item" data-value="2">2</div>
        <div class="scroller-item" data-value="3">3</div>
        <div class="scroller-item" data-value="4">4</div>
        <div class="scroller-item" data-value="5">5</div>
        <div class="scroller-item" data-value="6">6</div>
        <div class="scroller-item" data-value="7">7</div>
        <div class="scroller-item" data-value="8">8</div>
        <div class="scroller-item" data-value="9">9</div>
        <div class="scroller-item" data-value="10">10</div>
        <div class="scroller-item" data-value="11">11</div>
        <div class="scroller-item" data-value="12">12</div>
        <div class="scroller-item" data-value="13">13</div>
        <div class="scroller-item" data-value="14">14</div>
        <div class="scroller-item" data-value="15">15</div>
        <div class="scroller-item" data-value="16">16</div>
        <div class="scroller-item" data-value="17">17</div>
        <div class="scroller-item" data-value="18">18</div>
        <div class="scroller-item" data-value="19">19</div>
        <div class="scroller-item" data-value="20">20</div>
    </div>
</div>
* {
    margin: 0;
    padding: 0;
}
.scroller-component {
    display: block;
    position: relative;
    height: 238px;
    overflow: hidden;
    width: 100%;
}

.scroller-content {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    z-index: 1;
}

.scroller-mask {
    position: absolute;
    left: 0;
    top: 0;
    height: 100%;
    margin: 0 auto;
    width: 100%;
    z-index: 3;
    transform: translateZ(0px);
    background-image:
        linear-gradient(to bottom, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6)),
        linear-gradient(to top, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6));
    background-position: top, bottom;
    background-size: 100% 102px;
    background-repeat: no-repeat;
}

.scroller-item {
    text-align: center;
    font-size: 16px;
    height: 34px;
    line-height: 34px;
    color: #000;
}

.scroller-indicator {
    width: 100%;
    height: 34px;
    position: absolute;
    left: 0;
    top: 102px;
    z-index: 3;
    background-image:
        linear-gradient(to bottom, #d0d0d0, #d0d0d0, transparent, transparent),
        linear-gradient(to top, #d0d0d0, #d0d0d0, transparent, transparent);
    background-position: top, bottom;
    background-size: 100% 1px;
    background-repeat: no-repeat;
}

.scroller-item {
    line-clamp: 1;
    -webkit-line-clamp: 1;
    overflow: hidden;
    text-overflow: ellipsis;
}

css 代码主要作为样式展示,通过外链的方式引入。这里就不做过多的解释。

第二步实现手指滚动容器

1.添加手指触摸事件


let component = document.querySelector('[data-role=component]')

let touchStartHandler = (e) => { }
let touchMoveHandler = (e) => { }
let touchEndHandler = (e) => { }

component.addEventListener('touchstart', touchStartHandler)

component.addEventListener('touchmove', touchMoveHandler)

component.addEventListener('touchend', touchEndHandler)

这样当手指触摸到 component 插件容器的时候就会触发开始,移动,结束事件。

2.分析手指滑动容器移动效果

手指上滑内容上滑,手指下拉内容下拉。只需要控制 content 的位置改动的距离跟手指滑动的距离保持一致即可。这里用到了 transform 样式的 translate3d(x, y, z) 属性。其中 x, z 保持不变,y的值就是手指移动的值。

我们继续做拆解,当手指下拉时 content 位置下移就会跟手势保持一致。也就是 y 值变大(需要注意 y 轴正方向是往下的)。手指上拉正好与下拉上滑。当再次下拉或上拉时内容要在原来的基础上保持不变。因此我们需要一个全局变量 __scrollTop 保存这个值。这个值等于用户每次上拉下拉值的和,所以我们需要求出来用户每次上拉下拉的值。

拆解用户上拉的值,当用户手触摸到屏幕的时候就会触发 touchstart 事件,移动的时候会触发 touchmove 事件。离开的时候会触发 touchend 事件。用户上拉的初始值肯定是触发 touchstart 时手指的位置。结束值就是 touchend 时手指的位置。但是这样就不能够做到内容跟随手指实时运动了。所以需要拆解 touchmove 事件

touchmove 事件会在用户手指运动的时候不停的触发,也就相当于用户多次极小的上下移动。所以我们需要记录下来用户刚开始时触摸的位置。 __startTouchTop 。用手指当前位置减去刚开始触发位置就是用户移动的距离 __scrollTop。具体代码如下

let content = component.querySelector('[data-role=content]') // 内容容器
let __startTouchTop = 0 // 记录开始滚动的位置
let __scrollTop = 0 // 记录最终滚动的位置
// 这个方法下面马上讲解
let __callback = (top) => {
    const distance = top
    content.style.transform = 'translate3d(0, ' + distance + 'px, 0)'
}
// 这个方法下面马上讲解
let __publish = (top, animationDuration) => {
    __scrollTop = top
    __callback(top)
}
let touchStartHandler = (e) => {
    e.preventDefault()
    const target = e.touches ? e.touches[0] : e
    __startTouchTop = target.pageY
}
let touchMoveHandler = (e) => {
    const target = e.touches ? e.touches[0] : e
    let currentTouchTop = target.pageY
    let moveY = currentTouchTop - __startTouchTop
    let scrollTop = __scrollTop
    scrollTop = scrollTop + moveY
    __publish(scrollTop)
    __startTouchTop = currentTouchTop
}

注意1: touchstart 必须要记录触摸位置, touchend 可以不记录。因为用户第一次触摸的位置和下次触摸的位置在同一个地方的可能性几乎微乎其微,所以需要在 touchstart 里面重置触摸位置。否则当用户重新触摸的时候内容会闪动

**注意2:e.preventDefault() 方法是处理某些浏览器的兼容问题并且能够提高性能。像QQ浏览器用手指下拉的时候会出现浏览器描述导致方法失败。 可以参考文档 https://segmentfault.com/a/1190000014134234
https://www.cnblogs.com/ziyunfei/p/5545439.html**

上面的 touchMoveHandler 方法中出现了 __callback 的方法。这个方法是用来控制内容容器的位置的, __publish 方法是对改变容器位置的一层封装,可以实现跟用户的手指动作同步,也要实现用户手指离开之后位置不正确的判断等。目前先实现跟随用户手指移动

代码到这里,你用浏览器调节成手机模式应该已经可以做到内容跟随鼠标滚动了,但是还存在很多问题,下面会一一把这些问题修复

第三步,限制手指滑动最大值和最小值

目前用户可以无限上拉下拉,很明显是不对的。应该当第一个值稍微超越选中实线下方时就不能在下拉了,当最后一个值稍微超越选中实线上方时就不能在上拉了。所以我们需要俩个值最小滚动值: __minScrollTop 和最大滚动值: __maxScrollTop

计算方式应该是这个样子:用户下拉会产生一个最大值,而最大值应该是第一个元素下拉到中间的位置。中间应该就是元素容器中间的位置

let __maxScrollTop = component.clientHeight / 2 // 滚动最大值

最小值应该是用户上拉时最后一个元素达到中间的位置,因此应该是内容容器-最大值。

let __minScrollTop =  - (content.offsetHeight - __maxScrollTop) // 滚动最小值

最大值最小值有了,只需要在手指上拉下拉的过程中保证 __scrollTop 不大于或者不小于极值即可,因此在 touchMoveHandler 函数中补充如下代码

if (scrollTop > __maxScrollTop || scrollTop < __minScrollTop) {
    if (scrollTop > __maxScrollTop) {
        scrollTop = __maxScrollTop
    } else {
        scrollTop = __minScrollTop
    }
}

第四步元素的位置准确卡在选中实线中

目前手指抬起的时候元素停留的位置是存在问题,这个也很容易理解。因为一个元素是有高度的,当你手指移动的距离只要不是元素高度的整数倍他就会卡在选中实线上。因此我们只需要对移动的距离除以元素的高度进行四舍五入取整之后再乘以元素的高度就能够保证元素位置是元素高得的倍数了

let indicator = component.querySelector('[data-role=indicator]')
let __itemHeight = parseFloat(window.getComputedStyle(indicator).height)

let touchEndHandler = () => { 
    let scrollTop = Math.round(__scrollTop / __itemHeight).toFixed(5) * __itemHeight
    __publish(scrollTop)
}

这样子产生了俩个问题,一是当极值四舍五入之后超越了极值就会出错,二是元素跳动太大用户体验不好。所以需要处理极值情况和添加动画滑动效果

处理上面问题中产生的极值问题

我们新建一个函数 __scrollTo 专门解决元素位置不对的问题

// 滚动到正确位置的方法
let __scrollTo = (top) => {
    top = Math.round((top / __itemHeight).toFixed(5)) * __itemHeight
    let newTop = Math.max(Math.min(__maxScrollTop, top), __minScrollTop)
    if (top !== newTop) {
        if (newTop >= __maxScrollTop) {
            top = newTop - __itemHeight / 2
        } else {
            top = newTop + __itemHeight / 2
        }
    }
    __publish(top, 250) // 这里传入了第二个参数动画时长,先留一个伏笔。后面会讲
}

简单分析一下,函数内第一行跟之前的一样。对位置进行四舍五入变成元素高度的倍数。第二行判断元素是否大于极值,如果大于最大值就取最大值,小于最小值就取最小值。当滚动值跟新的滚动值不一样的时候说明用户移动超过了极值。然后进行处理。大于等于最大值的时候元素的位置正好超出半个元素高度的,所以减掉高度的一半,小于最小值的时候恰好相反。添加一半

添加动画滑动效果

这个比较麻烦,关于动画效果是可以单独开一章来说的。这里我简单说一下我这个动画的思路吧。尽量长话短说。

首先讲解一下动画实现的原理,动画可以理解为多张连续的照片快速移动超过眼睛可以捕获的速度就会形成连贯的动作。这就是我理解的动画,像上面的 touchMoveHandler 方法其实是会被多次调用的,而且调用频率非常的高,高到了几毫秒调用一次,这个速度你肉眼肯定是分辨不出来的,而且每次移动的距离贼短。所以你看起来就有了跟随手指滚动的效果

所以当手指抬起的时候发现位置不正确这个时候应该实现一个滚动到正确位置的减速动画效果。这里我直接将 vux 里面的 animate.js 文件简化了一下直接拿过来用了

let running = {} // 运行
let counter = 1 // 计时器
let desiredFrames = 60 // 每秒多少帧
let millisecondsPerSecond = 1000 // 每秒的毫秒数

const Animate = {
  // 停止动画
  stop (id) {
    var cleared = running[id] != null
    if (cleared) {
      running[id] = null
    }
    return cleared
  },

  // 判断给定的动画是否还在运行
  isRunning (id) {
    return running[id] != null
  },
  start (stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
    let start = Date.now()
    let percent = 0 // 百分比
    let id = counter++
    let dropCounter = 0

    let step = function () {
      let now = Date.now()

      if (!running[id] || (verifyCallback && !verifyCallback(id))) {
        running[id] = null
        completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false)
        return
      }

      if (duration) {
        percent = (now - start) / duration
        if (percent > 1) {
          percent = 1
        }
      }
      let value = easingMethod ? easingMethod(percent) : percent
      if (percent !== 1 && ( !verifyCallback || verifyCallback(id))) {
        stepCallback(value)
        window.requestAnimationFrame(step)
      }
    }

    running[id] = true
    window.requestAnimationFrame(step)
    return id
  }
}

以上代码作为一个js外链单独引入,不知道取什么名就用 animate.js 好了。

简单讲解一下,主要是弄了一个叫 Animate 的对象,里面包含三个属性 stop, isRunning, start。 分别是停止动画,动画是否在执行,开始一个动画。start 是关键,因为其他俩个函数在这个项目中我都没有用过,哈哈。

start 函数包含很多个参数,stepCallback:每次动画执行的时候用户处理的界面元素滚动逻辑;verifyCallback:验证动画是否还需要进行的函数;completedCallback:动画完成时的回调函数;duration:动画时长;easingMethod:规定动画的运动方式,像快进慢出,快进快出等等;root:不用管了,没用到。

结束动画有俩种方式,第一种是传入的动画时长达成,另一种是验证动画是否还需要执行的函数验证通过。否则动画会一直运动

有了动画函数了,接下来就是如何使用了。这里我们补充一下 __publish 函数,并且添加一个是否开启动画的全局变量 __isAnimating 和 俩个曲线函数 easeOutCubic, easeInOutCubic

let __isAnimating = false // 是否开启动画
// 开始快后来慢的渐变曲线
let easeOutCubic = (pos) => {
    return (Math.pow((pos - 1), 3) + 1)
}
// 以满足开始和结束的动画
let easeInOutCubic = (pos) => {
    if ((pos /= 0.5) < 1) {
        return 0.5 * Math.pow(pos, 3)
    }
    return 0.5 * (Math.pow((pos - 2), 3) + 2)
}

let __publish = (top, animationDuration) => {
    if (animationDuration) {
        let oldTop = __scrollTop
        let diffTop = top - oldTop
        let wasAnimating = __isAnimating
        let step = function (percent) {
            __scrollTop = oldTop + (diffTop * percent)
            __callback(__scrollTop)
        }
        let verify = function (id) {
            return __isAnimating === id
        }
        let completed = function (renderedFramesPerSecond, animationId, wasFinished) {
            if (animationId === __isAnimating) {
                __isAnimating = false
            }
        }
        __isAnimating = Animate.start(step, verify, completed, animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic)
    } else {
        __scrollTop = top
        __callback(top)
    }
}

将上面的代码补充完整你就会发现滚动到正确位置的动画效果实现了,下面就讲讲实现的原理。

这里按照函数执行的顺序讲解吧。 首先是定义的几个变量, oldTop:用来保存元素的错误位置; diffTop: 传入的 top 是元素滚动的正确位置; step, verify, completed 是 Animate 对象需要的三个回调函数。里面的参数先不用管后面会讲,最下面给 __isAnimating 付了个值。 Animate.start 函数是有返回值的,返回值是当前动画的ID

其中需要注意 wasAnimating ? easeOutCubic : easeInOutCubic 这个。意思就是如果原来的动画存在就将 easeInOutCubic(俩头慢中间快的参数传入进去)函数传入进去, 如果不存在就传入进去 easeOutCubic(开始快后来慢)函数传入进去。符合的场景就是你手指快速滑动抬起动画会执行一段时间吧,这个过程动画就是从快到慢的过程,然后动画还没结束你又接着快速滑动是不是又从慢到快了。如果你不接着执行是不是动画就由快到慢结束了。这里为啥传入这俩个参数就不讲解了,完全可以再开一篇博客进行讲解比较麻烦。

step函数,接受一个 percent 翻译过来是百分比的意思。 下面的第一行代码

__scrollTop = oldTop + (diffTop * percent)

可以理解成, 老的位置 + 移动的距离 * 百分比 就是新的位置。百分比一直增大当百分比为百分之百的时候 __scrollTop === top。就实现了一个错误位置到正确位置的过度。

百分比的计算方式是根据时间来计算的,然后被动画曲线进行了加工

if (duration) {
    percent = (now - start) / duration
    if (percent > 1) {
      percent = 1
    }
}
let value = easingMethod ? easingMethod(percent) : percent

上面的是核心代码。start 是调用Animate.start属性的时候记录的一个当前时间,now是内部函数执行的时候记录的一个当前时间。 now - start 就是经过了多长时间,除以 duration动画时长就可以得出动画时长的百分比。下面判断 easingMethod 是否传入如果传入了就对本来匀速增加的百分比进行加工变成了动画曲线变化的百分比。

首先是 step 函数,每次运动调用的函数。接受了一个 percent ,翻译过来是百分比意思。 在外面我定了一个几个局部变量,分别是 oldTop: , , 正确位置减掉错误位置也就是元素滚动的距离。在 step 函数里赋予 __scrollTop 新值

step函数接受了一个叫百分比的参数。 用处就是当元素不在正确位置的时候会产生一个值 __scrollTop, 而元素应该的正确位置的值是 top,元素移动的距离就是 diffTop = top - oldTop 如何一步一步的移动到这个位置呢。就通过动画函数穿过来的这个百分比参数。这也是为啥在 __scrollTo 方法中调用 __publish 时加入第二个参数动画时长的原因了,这样就实现了一个自由滚动的动画

verify函数接受一个当前动画的id参数,验证规则就是 __isAnimating === id 时说明开启了下一个动画 __isAnimating 就会改变。导致验证失败,这个时候就会停止上一个动画

completed函数接受好几个参数,第一个参数是每秒多少帧,第二个参数是当前动画id,第三个参数是完成状态。这里主要用到了第二个参数当前动画id。动画完成的时候应该奖动画id变为false否则会一直走验证的逻辑。

第五步快速短暂触摸,让内容自己快速动起来

像目前内容滑动的距离基本是等于用户手指触摸的距离的,这样就跟实际使用不符合,实际中手指使劲一滑内容也会蹭蹭的滚动。就目前这个样子内容一多也能累死用户,所以需要添加用户使劲滑动内容快速滚动起来的逻辑

首先内容自己快速动起来很明显是有个触发条件的,这里的触发条件是 touchEndHandler 函数执行时的时间减去当最后一次执行 touchMoveHandler 函数的时间小于100毫秒。满足这种状态我们认为用户开启快速滚动状态。所以添加一个全局变量 __lastTouchMove 来记录最后一次执行 touchMoveHandler 函数的时间。

知道应该快速滚动了,如何判断应该滚动多长的距离呢?想一下当前的条件,有一个 __lastTouchMove 和执行 touchEndHandler 函数的时间。这俩个是不是能够的出来一个时间差。在想一下是不是有个 __scrollTop 滚动的位置,如果在获取到上一个滚动的位置是不是能够得到一个位置差。那位置 / 时间是等于速度的。我们让 __scrollTop + 速度 是不是可以得到新的位置。然后我们一直减小速度捡到最后等于 0 是不是就得到了滚动的位置,并且能够根据用户的快速滑动情况的出来应该滚动多长的距离,用户滑的越快速度越快距离越远,相反的用户滑动的速度越慢距离越近

遗憾的是在 touchEndHandler 函数中拿不到目标移动的距离 pageY。所以我们需要在 touchMoveHandler 方法中做手脚,去记录每次执行这个方法时的时间和位置。所以我们再添加一个全局变量 __positions 为数组类型。

// 上面提到的俩个全局变量的代码
let __lastTouchMove = 0 // 最后滚动时间记录
let __positions = [] // 记录位置和时间

然后我们将增加 __positions 的代码添加到 touchMoveHandler 方法中

if (__positions.length > 40) {
    __positions.splice(0, 20)
}
__positions.push(scrollTop, e.timeStamp)

__publish(scrollTop)

__startTouchTop = currentTouchTop
__lastTouchMove = e.timeStamp

其中如果 __positions 的长度超过40我们就取后20个。因为数组太大占用内存,而且循环遍历的时候还非常浪费时间。根据上面的逻辑我们手指快速移动不会取时间过长的数据,所以20足够了。当有了宝贵的位置和时间数据我们就需要在 touchEndHandler 方法中分析出来移动的速度了。这里我将完整的代码先切出来。

let __deceleratingMove = 0 // 减速状态每帧移动的距离
let __isDecelerating = false // 是否开启减速状态
let touchEndHandler = (e) => {
    if (e.timeStamp - __lastTouchMove < 100) { // 如果抬起时间和最后移动时间小于 100 证明快速滚动过
        let positions = __positions
        let endPos = positions.length - 1
        let startPos = endPos
        // 由于保存的时候位置跟时间都保存了, 所以 i -= 2
        // positions[i] > (self.__lastTouchMove - 100) 判断是从什么时候开始的快速滑动
        for (let i = endPos; i > 0 && positions[i] > (__lastTouchMove - 100); i -= 2) {
            startPos = i
        }
        if (startPos !== endPos) {
            // 计算这两点之间的相对运动
            let timeOffset = positions[endPos] - positions[startPos] // 快速开始时间 - 结束滚动时间
            let movedTop = __scrollTop - positions[startPos - 1] // 最终距离 - 快速开始距离
            
            __deceleratingMove = movedTop / timeOffset * (1000 / 60) // 1000 / 60 代表 1秒60每帧 也就是 60fps。玩游戏的可能理解 60fps是啥意思
    
            let minVelocityToStartDeceleration = 4 // 开始减速的最小速度 
            // 只有速度大于最小加速速度时才会出现下面的动画
            if (Math.abs(__deceleratingMove) > minVelocityToStartDeceleration) {
                __startDeceleration()
            }
        }
    }
    if (!__isDecelerating) {
        __scrollTo(__scrollTop)
    }
    
    __positions.length = 0
}

新添加了俩个全局变量运动速度和减速状态记录。当减速状态为true的时候肯定不能执行 __scrollTo 方法的因为这俩个方法是冲突的。所以需要 __isDecelerating 记录一下。里面新定义了一个函数 __startDeceleration。 我们的减速方法也主要是在这个方法里面实现的。给你一下代码

// 开始减速动画
let __startDeceleration = () => {
    let step = () => {
        let scrollTop = __scrollTop + __deceleratingMove
        let scrollTopFixed = Math.max(Math.min(__maxScrollTop, scrollTop), __minScrollTop) // 不小于最小值,不大于最大值
        if (scrollTopFixed !== scrollTop) {
            scrollTop = scrollTopFixed
            __deceleratingMove = 0
        }
        if (Math.abs(__deceleratingMove) <= 1) {
            if (Math.abs(scrollTop % __itemHeight) < 1) {
                __deceleratingMove = 0
            }
        } else {
            __deceleratingMove *= 0.95
        }
        __publish(scrollTop)
    }
    let minVelocityToKeepDecelerating = 0.5
    let verify = () => {
        // 保持减速运行需要多少速度
        let shouldContinue = Math.abs(__deceleratingMove) >= minVelocityToKeepDecelerating
        return shouldContinue
    }
    let completed = function (renderedFramesPerSecond, animationId, wasFinished) {
        __isDecelerating = false
        if (__scrollTop <= __minScrollTop || __scrollTop >= __maxScrollTop) {
            __scrollTo(__scrollTop)
            return
        }
    }
    __isDecelerating = Animate.start(step, verify, completed)
}

当你把这些代码都加进去的时候,选择器插件基本上就已经完成了。下面讲解一下这段让你头痛的代码。

这里面用到了动画,所以肯定包含三大回调函数 step, verify, completed。然后一个一个讲解一下

step函数:这个函数是让内容一步一步运动的,这个函数基本上跟滚动到正确位置的函数相似度很高。 新的位置是老位置 __scrollTop 加上每帧移动的位置 __deceleratingMove。 然后让每帧移动的位置一直减少,但是需要注意 scrollTop 不能超出极值,所以做了最大值最小值判断当到达极值的时候就将 __deceleratingMove 赋值为0 。

if (Math.abs(__deceleratingMove) <= 1) {
    if (Math.abs(scrollTop % __itemHeight) < 1) {
        __deceleratingMove = 0
    }
}

这段代码,你可能佷懵。他的作用是当滚动的位置没有到达极值的时候如何让他卡在正确位置上。 Math.abs(__deceleratingMove) 这是每帧移动的距离的绝对值。当他小于1的时候说明移动的距离已经非常小了,用户基本上都察觉不到移动了。然后再用新位置对元素高度取余,如果余数为0表示正好卡在正确位置上,但是即使稍微比 0 大那么一丢丢也看不出来,而且基本不会那么巧取到 0,所以当余数满足小于 1 的时候讲每帧移动的距离赋值为0.

verify函数:定义了一个最小每帧移动距离的局部变量 minVelocityToKeepDecelerating, 当 __deceleratingMove 值小于他的时候说明用户基本上不会发现内容还在移动可以停下来了。

completed函数:既然是完成函数就一定要将 __isDecelerating 参数变为false,否则下次进行的不是快速移动内容就没法跑到正确位置上了。这里多加了一步是否是极值的判断,如果是极值就执行 __scrollTo 函数到正确位置上。

下载正确代码

代码的正确顺序实在是不好再文中体现出来,所以我将正确代码放到了我的github上,让大家不知道放到哪里的时候好有个参照

https://github.com/mrxu0/nativePhoneUI

感谢

这个项目是参考的 vux 做出来的。基本上代码都是对 vux 里面picker的解读。为了更加凸显picker的核心代码,我在里面删除了很多东西像兼容性,双指滑动的代码。有兴趣可以去看看这个项目
https://github.com/airyland/vux

注意:这个样式虽然已经满足大部分项目需求了,但是还是不够漂亮。我看过很多手机端pickder的插件。发现京东的nutui的是最漂亮的,他里面用到了css3的旋转特性把他做的想一个滚轮一样。有兴趣的可以学习更精进一波。
https://github.com/jdf2e/nutui

后期规划

选择器控件只是开始,后面我会将他衍生出来地区级联控件,日期控件。还会扩展轮播图,上拉加载下拉刷新,浮层等等控件。并且集成到vue中作为一个手机端框架。有兴趣的可以关注我的另外一个项目。目前是什么都没有的,所以非常适合想要学习的人一步一步跟进。
https://github.com/mrxu0/iphone-ui

交流反馈

欢迎大家加QQ群交流反馈:954917384

查看原文

李毅超 发布了文章 · 2019-10-08

【免费】angular8高级实战教程—网易云音乐pc端

自制angular8实战教程,先上链接:

历时个把月,本想出个单一功能的教程,没想一开始就控制不住了,到最后时长竟高达30多个小时,

为什么是angular?

angular是我的第一个框架,所谓先入为主,即使工作中怕是再难用上,也不会把它丢掉,而且angular用户是痛苦的,至少在国内,不论是文档、生态、百度、教程等都全面被vue和react压制,并非angular技不如人,只因google太任性。本教程也算是为推进angular做点贡献吧,这应该是前端框架中,最给力的免费教程了。

做什么?

用angular8仿造网易云音乐pc端的部分功能,包括:歌单、歌曲、歌手和会员的登录注册等,并实现网易云核心的播放器功能。

能学到什么?

主技术栈:angular8 + ngrx8 + ng-zorro + material/cdk,包括但不限于:

  • ng-template,ng-content,ng-container
  • ng模块化设计
  • proxy,http拦截器
  • 依赖注入
  • 自定义指令和管道
  • 自定义表单控件
  • 动态组件
  • 各种rxjs操作符
  • material/cdk
  • 变更检测策略
  • ngrx8
  • ...

课程特色?

本课程全程都在实战,在开发过程中会尽力覆盖ng的各种api,项目的模块化、目录设计和组件化等都是以真实项目标准来做的,可复用到日常工作的各项目中去,代码极度精简,期间更有徒手造轮子的过程,是一门学习框架和提升基本功的双休课程。
源码也分好了章节上传到github:
master分支是最终完成的代码
https://github.com/lycHub/ng-wyy

需要的基本知识?

  • typescript
  • rxjs
  • angular基本api的使用(重要)

学完后能达到什么水平?

由于本课程会尽可能多的使用angular高级api,如果能完全掌握,那在使用层面已经非常优秀了。完全可以独自用angular胜任网易云音乐官网这种难度的项目

讲授方式?

手写每行ts和html,样式部分复制做好的。

很遗憾无法上传到慕课网,因为事先没有了解清楚,推荐大家去网易云课堂或B站观看:

后续计划?

如果日常够稳定的话,会持续录教程,现在年底估计是没啥时间了,后面再录教程也一定是走高难度路线,基础视频大概率是不会出的,这里也有了基本规划,最快等明年初vue3发布后(个人觉的有点悬),打算出一个vue3 + ts的造轮子为主的移动端课程,也就是不用任何ui框架徒手实现移动端日历、滑块、picker等复杂组件,这也是我入行第一年的经历, 从定位上难度就会远高于本课程,希望明年再见吧!!!

查看原文

赞 12 收藏 3 评论 3

李毅超 收藏了文章 · 2019-08-15

vscode这篇就够了

前言

本文档用于记录vscode的使用技巧,持续更新中

github: https://github.com/abc-club/f...

更多前端资源尽在 https://github.com/abc-club/f...

快捷键

搜索文本

搜索当前文件

Windows: Ctrl + F Mac: Command + F

搜索所有文件

Windows: Ctrl + Shift + F Mac: Command + Shift + F

打开关闭了的页面

Windows: Ctrl + Shift + T Mac: command + Shift + T

终端

打开关闭终端

Ctrl + `

终端+1

Ctrl + Shift + `

终端分屏

Mac: command + \

切换不同的项目

Mac: command+ `

切换选项卡

Windows: Ctrl + Alt +右箭头  Mac: Control + Option +右箭头
Windows: Ctrl + Alt +左箭头  Mac: Control + Option +左箭头

批量替换当前文件中所有匹配的文本

Windows: Ctrl + F2  Mac: command + F2

向上/向下移动一行

Windows: Alt + 向上箭头  Mac: option+ 向上箭头

复制光标向上或者向上批量添加内容

Windows: Ctrl + Alt +向上箭头  Mac: command + Option +向上箭头

插件

带星的为极力推荐的插件

* Auto Close Tag

* Auto Rename Tag

修改html标签时自动帮我们重命名

* Document This

注释js代码

* EditorConfig for VS Code

这个插件会用项目里的.editorconfig文件覆盖vscode的设置

* ESLint

eslint检查代码规范

* Git History

git历史

* GitLens — Git supercharged

每个文件每行都会显示修改信息,非常推荐

* npm Dependency Links

cmd/ctrl+click跳转到npmjs.com

* npm Intellisense

自动填写npm模块

* npm-import-package-version

显示npm包引入的版本号,支持js ts vue

* Path Intellisense

自动填写文件路径

* Prettier - Code formatter

美化

* Settings Sync

同步vscode设置,非常推荐

* TODO Highlight

* Vetur

使用vue就装上

* vscode-fileheader

快速生成文件头注释

* vscode-icons

文件根据不同后缀显示不同的图标

* vue

Auto Import - ES6, TS, JSX, TSX

自动引入依赖包

Beautify

美化代码

Bracket Pair Colorizer

Check Updates of NPM Packages

在后台检查npm包是否可以更新,并会提示你

ES7 React/Redux/GraphQL/React-Native snippets

filesize

显示文件大小

Full React/React Native/React Router/Redux/GraphQL/ES7/Testing/PropTypes snippets

Highlight Line

高亮选中的行

Highlight Matching Tag

高亮匹配的标签

HTML CSS Support

HTML SCSS Support

HTML Snippets

html tag wrapper

用div包裹选中的代码,ctrl+i

Image preview

在编辑器左侧显示图片缩略图

Live Server

起一个服务

npm

支持跑npm脚本

Npm Dependency

更新依赖包

open-in-browser

Partial Diff

对比不同

Rainbow Brackets

匹配括号

SVG Viewer

Toggle Quotes

TSLint

Turbo Console Log

快速的添加console

View In Browser

vscode-elm-jump

vscode-styled-jsx

WakaTime

记录你用vscode工作的时间,项目等

参考文档

https://juejin.im/post/5d34fd...

查看原文

李毅超 发布了文章 · 2019-08-01

修改lyric-parser,解析网易云音乐歌词(针对angular用户)

最近在用angular8仿网易云音乐的pc端,考虑出个ng相关的教程

写到歌词的时候开始直接用了黄大仙的lyric-parser,发现不太适用网易云的歌词,主要有如下几点:

  • 网易云有中英两套歌词
  • 很多歌曲中英文的歌词开头几行对不上
  • 歌词的微秒位可能有3位数的(QQ音乐只有两位),造成时间误差

其中第二点的情况比较棘手,比如英文歌词比中文的开头多几行,比如这种:

clipboard.png

反过来也有可能。
所以需要改下lyric-parser中的initLines方法。
lyric字段是主歌词,我只要保证主歌词完整,tlyric每行只要对应上lyric就行了,所以我先把tlyric开头多余的行给过滤掉了:

clipboard.png

然后把lyric字段开头多余的几行单独抽出来,这样lyric和tlyric就一一对应了。再利用rxjs中的zip方法可以方便地处理这种对应的数据流:

clipboard.png

zip方法会严格对应地输出流,把每行的中文,英文和歌词的时间戳保存起来就行了:

clipboard.png

其他部分不用怎么修改,angular用户可以直接copy下来试下,项目地址

查看原文

赞 1 收藏 0 评论 1

李毅超 发布了文章 · 2019-06-30

用原生TypeScript造轮子(六) Tree

ui参考primeNG: http://primefaces.org/primeng...
ts开发环境:https://github.com/lycHub/ts-...
目录

简介

本节demo源码
参考ivew tree的内部实现,完成其部分功能
已实现的功能:

  • 默认展开
  • 默认选中
  • 单/多选

基本思路

用户需要传入一个相对固定的数据结构,比如:

const data = [
  {
    title: 'parent 1',
    expand: true,
    children: [
      {
        title: 'parent 1-1',
        expand: true,
        selected: true,
        children: [
          {
            title: 'leaf 1-1-1'
          },
          {
            title: 'leaf 1-1-2'
          }
        ]
      },
      {
        title: 'parent 1-2',
        children: [
          {
            title: 'leaf 1-2-1'
          },
          {
            title: 'leaf 1-2-1'
          }
        ]
      }
    ]
  },
  {
    title: 'parent 2',
    expand: true,
    children: [
      {
        title: 'parent 2-1',
        children: [
          {
            title: 'leaf 2-1-1'
          },
          {
            title: 'leaf 2-1-2',
            selected: true
          }
        ]
      },
      {
        title: 'parent 2-2',
        expand: true,
        children: [
          {
            title: 'leaf 2-2-1'
          },
          {
            title: 'leaf 2-2-2'
          }
        ]
      }
    ]
  }
];

然后根据这个data, 用递归函数渲染出dom,每个children都对应一个ul:

clipboard.png

所以最重要的就是写出递归函数,demo中是这段:

clipboard.png

这里只是组装dom结构,还需要再写个递归函数,把dom和用户传入的data对应起来:

clipboard.png

这个函数给每item加了个nodeKey作为标识,在上面渲染dom,只要把这个nodeKey存在dom中,

clipboard.png

那么在点击选中时,就能映射到对应的item数据了:

clipboard.png

还有个点是,ts中对于地归类的数据类型,可以如下设置:

export type DataTree = {
  title: string;
  expand?: boolean;
  selected?: boolean;
  nodeKey?: number;
  children?: DataTree[];
}
查看原文

赞 0 收藏 0 评论 0

李毅超 发布了文章 · 2019-06-29

用原生TypeScript造轮子(六) 取色器

ui参考primeNG: http://primefaces.org/primeng...
ts开发环境:https://github.com/lycHub/ts-...
目录

简介

参考primeNG的实现,可以hex,rgh,hsb三种颜色模式互相转换
本节demo源码

基本思路

dom分成两部分,左边的面板有一块黑白底图作为背景,右边长条也有张彩色底图作为背景

clipboard.png

右边长条选中的颜色,设为左面板的背景色,这样左面板可跟随右侧变色

clipboard.png

其中最重要的就算3种颜色的转换,转换部分是直接copy的源码,具体详见代码。

查看原文

赞 0 收藏 0 评论 0

李毅超 发布了文章 · 2019-06-29

用原生TypeScript造轮子(五) 滑块Slider

ui参考primeNG: http://primefaces.org/primeng...
ts开发环境:https://github.com/lycHub/ts-...
目录

简介

仿造我之前用vue封装的iv-slider, 实现了其横向的一些功能:
本节demo源码
已实现的功能:

  • 支持移动端
  • 单/双滑块
  • 设置步长
  • 显示断点
  • 自定义事件

基本思路

先看dom,在鼠标滑动时,单滑块只要不断改变left值和蓝色条的width即可,双滑块还需要另外设置蓝色调的left

clipboard.png

滑块移动的位置和当前值有一个比例关系:
(当前值 / 数值范围) = (鼠标当前位置 - 鼠标起始位置) / slider总长度
其中数值范围是用户设置的max - min,默认100 - 0。

我这里是在求出当前值后再换算成dom值,滑块的left用的是百分比。

clipboard.png

其它功能如刻度,垂直,tooltip等可以参考iv-slider

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • SegmentFault 讲师
  • 获得 73 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-04-15
个人主页被 1.1k 人浏览