Design and implementation of Picker component

preface

Today's topic is the design and implementation of NutUI Picker component, which is NutUI Is a handler component of the. It is used to display a series of value sets. Users can scroll to select an item in the set, or support multiple series of value sets for users to select respectively. Let's use a rendering to see what functions the component implements.

Speaking of NutUI , some people may not know much about it. Let's briefly introduce it first. NutUI It is a set of JD style mobile Vue component library, which develops and serves enterprise level front, middle and back end products for mobile Web interface. adopt NutUI , you can quickly build a uniform style page to improve development efficiency. At present, there are 50+ components, which are widely used in various mobile terminal services of JD.

Next, we will expand today's content through the following topics:

  • Why package components
  • Implementation principle of NutUI Picker component
  • Problems encountered

1, Why package components

When the business reaches a certain scale, it will encounter many similar functional interfaces. Each redevelopment will affect the development efficiency, and these similar codes may have hidden problems. Once exposed, we need to spend a lot of time dealing with the same codes in the business. If we rationalize these same codes, encapsulate components and make multiple calls, we will find that the development efficiency has made a qualitative leap.

Let's take a look at the benefits of the encapsulated components in the following figure:

Encapsulating components can not only make collaborative development efficient and standardized, but also bring more convenience for subsequent business expansion.

2, Implementation principle of NutUI Picker component

This component is still common in daily business requirements. It can not only carry simple tab functions, but also meet the more cumbersome date and time selection, or cascade address selection functions. The date time component based on the picker component is also encapsulated and accessible to interested users NutUI component library View.

From the foreword of the article, we have roughly understood what functions the picker component has realized. It realizes a certain item of the selected selection set through three-dimensional rotation similar to the wheel.

Let's first look at the directory structure of the component source code:

We mainly focus on the last three documents.
Based on the principle of proximity, we put the relevant documents in the same directory. Based on the principle of single responsibility, we granulate the components to ensure that the components are as simple and universal as possible. Divide the picker component into the parent component picker Vue and sub component picker slot Vue, the sub assembly is only responsible for the interactive processing of rollers. The parent component is responsible for processing business class logic.

Subassembly roller section

1. Let's take a look at the division of dom

<div class="nut-picker-list">
    <div class="nut-picker-roller" ref="roller">
        <div class="nut-picker-roller-item" 
            :class="{'nut-picker-roller-item-hidden': isHidden(index + 1)}"
            v-for="(item,index) in listData"
            :style="setRollerStyle(index + 1)"
            :key="item.label"
        >
            {{item.value}}
        </div>
    </div>
    <div class="nut-picker-content">
        <div class="nut-picker-list-panel" ref="list">
            <div class="nut-picker-item" 
                 v-for="(item,index) in listData"
                 :key="item.label "
            >
                 {{item.value }}
            </div>
        </div>
    </div>
    <div class="nut-picker-indicator"></div>
</div>
  • Nut picker indicator: split line
  • Nut picker content: highlight the selected area
  • Nut picker roller: roller area

Don't want to see the code? "Waiter, above!"

2. css part

Set the nut picker indicator at the highest level to avoid being covered

.nut-picker-indicator{
    ...
    z-index: 3;
}

Nut picker roller area

.nut-picker-roller{
    z-index: 1;
    transform-style: preserve-3d;
    ...
    .nut-picker-roller-item{
        backface-visibility: hidden;
        position: absolute;
        top: 0;
        ...
    }
}

To achieve some 3D effects, transform style:preserve-3d; Is essential. In general, this attribute is applied to the parent element of the 3D transformation, that is, the stage element. This gives the child elements the effect of 3D attributes. In the 3D world of CSS, we can see the elements behind by default. In order to be practical, we often make the elements behind invisible, so we set the child element backface visibility: hidden;
It is worth noting that if transform style: preserve-3d is set, child element overflow cannot be prevented. If overflow:hidden is set, transform style: preserve-3d will be invalid.

We simulate the rotation of the wheel to achieve the interactive effect of the components, and use a side view to have a more intuitive look.

Next, let's look at how to implement it.

First, you need to simulate a sphere, set each item of the selection set (hereinafter referred to as "roller item") to position:absolute, share the same center point, that is, the ball center, and then stack it here in turn.

Let's review some basic knowledge first. The translate3d() function can make an element move in three-dimensional space. The characteristic of this deformation is that the coordinates of the three-dimensional vector are used to define how much the element moves in each direction. When the z-axis value is larger, the element is also closer to the viewer. We set the z-axis so that both ends of the wheel item reach the surface of the sphere. The size of the z-axis is equivalent to the radius of the sphere. Because we set the height of the visible area to 260, we set the radius to 104. If the radius is too small, we need to wear a high-power magnifying glass to find the wheel item. If the radius is too large, the wheel item will go behind our heads, You can't let such a terrible thing happen! The so-called distance produces beauty, so keeping an appropriate distance (80%) is the most beautiful.

setRollerStyle(index) {
    return `translate3d(0px, 0px, 104px)`;
}

At this time, we found that all the roller items changed from collectively stacking the ball center to stacking to a certain two points of the ball, and we need to lay them out according to the perimeter. At this time, we need to use the rotate3d() attribute. Our wheel rotates around the X axis, so we can set the rotate3d(1, 0, 0, a) of the X axis. A is an angle value, which is used to specify the rotation angle of the element in the 3D space. If the value is positive, the element rotates clockwise, otherwise the element rotates counterclockwise. How to set this angle? It can be inferred from a center angle formula that the degree of the center angle is equal to the degree of the arc to which it corresponds. Our radius is 104 and the arc length is 36 (our preset display area), so the angle a is rounded to 20. Is there a feeling of being said to be encircled? Let's have a more intuitive understanding through a picture.

Using the above analysis, we can dynamically set the final position of the wheel item.

setRollerStyle(index) {
    return `transform: rotate3d(1, 0, 0, ${-this.rotation * index}deg) translate3d(0px, 0px, 104px)`;
}

It should be noted that there may be a large number of wheel items, and the possibility of more than one circle is great. However, we can neither show the specified number to the user, nor show all of them, resulting in overlapping problems. At this time, we need to hide the excess part. We know that the angle a is 20 degrees and the circle is 360 degrees, so we can display 18 at most. We show 8 in front and 9 in the back based on the current center.

isHidden(index) {
    return (index >= this.currIndex + 9 || index <= this.currIndex - 8) ? true : false;
}

3. Add event

Finally, let's add a sliding event. First, get the DOM element associated with the Vue instance, and set the touchstart, touchmove, and touchend events. Note that we should remember to destroy these events in the beforeDestroy event.

The touchstart event is used to record the start point, the touchmove and touchend events are used to record the rolling end point, calculate the difference, and dynamically set the rolling distance and rolling angle of the outermost element of the wheel. During rolling, the rolling distance shall be corrected to ensure that the final rolling distance is a multiple of lineSpacing (the height of the roller item 36).

We also added the elasticity effect to allow touchmove to scroll beyond the scrolling range, and then correct the position of the first and last items in the touchend event.

Let's take a look at the implementation.

setMove(move, type, time) {
    let updateMove = move + this.transformY;
    if (type === 'end') { // touchend scrolling
    
        // Correction for exceeding the limited rolling distance
        if (updateMove > 0) {
            updateMove = 0;
        }
        if (updateMove < -(this.listData.length - 1) * this.lineSpacing) {
            updateMove = -(this.listData.length - 1) * this.lineSpacing;
        }

        // Set the scrolling distance to the multiple of lineSpacing
        let endMove = Math.round(updateMove / this.lineSpacing) * this.lineSpacing;
        let deg = `${(Math.abs(Math.round(endMove / this.lineSpacing)) + 1) * this.rotation}deg`;
        this.setTransform(endMove, type, time, deg);
        this.timer = setTimeout(() => {
            this.setChooseValue(endMove);
        }, time / 2); 

        this.currIndex = (Math.abs(Math.round(endMove/ this.lineSpacing)) + 1);
    } else { // touchmove scrolling
        let deg = '0deg';
        if (updateMove < 0) {
            deg = `${(Math.abs(updateMove / this.lineSpacing) + 1) * this.rotation}deg`;
        } else {
            deg = `${((-updateMove / this.lineSpacing) + 1) * this.rotation}deg`;
        }
        this.setTransform(updateMove, null, null, deg);
        this.currIndex = (Math.abs(Math.round(updateMove/ this.lineSpacing)) + 1);
    }
},

In touchend, a transition "jog function" is added to the parent element of the wheel to simulate the effect of inertial rolling.

setTransform(translateY = 0, type, time = 1000, deg) {
    this.$refs.roller.style.transition =  type === 'end' ? `transform ${time}ms cubic-bezier(0.19, 1, 0.22, 1)` : '';
    this.$refs.roller.style.transform = `rotate3d(1, 0, 0, ${deg})`;
}

Through the above content, our roller effect has been basically formed. But we also want to highlight the current area with the time selector on ios. How can we achieve this?

We tried the following three methods.

First, when the scroll wheel item stays in the highlighted area, the font will change. However, practice has found that the font can only be changed at the end of scrolling. It cannot be set during scrolling, and the experience is not friendly.

Second, can you skillfully use CSS, use the background gradient and background size to complete the gradient, and use the mask to achieve it!

.nut-picker-mask{
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-image: linear-gradient(180deg,hsla(0,0%,100%,.9),hsla(0,0%,100%,.6)),linear-gradient(0deg,hsla(0,0%,100%,.9),hsla(0,0%,100%,.6));
    background-position: top, bottom;
    background-size: 100% 108px;
    background-repeat: no-repeat;
    z-index: 3;
}

Here, the background is set to yellow, so that we can see the effect.

I think it's OK. Is this done?

We simulate that everything is normal on the pc, but a strange picture appears on the real machine. When the slide up pops up, the mask will delay the display and affect the experience effect. Only when the up sliding transition effect is prohibited can it be displayed normally. It is impossible to remove the up sliding effect. We can only consider other methods.

Third, whether an auxiliary scroll can be set, that is, the highlighted area mentioned above, which is covered on the scroll wheel. The height of each element inside is equal to the height of the visible area. When the scroll wheel slides, the list elements inside the highlighted area slide with it.

Practice has proved that this method can avoid the disadvantages of the above two methods and perfectly solve our needs. Let's take a look at the specific implementation method.

.nut-picker-content {
    position: absolute;
    height: 36px;
    ...
    .nut-picker-roller-item{
        height: 36px;
        ...
    }
}

Then, in the setTransform function above, add the scrolling effect of the highlight area.

setTransform(translateY = 0, type, time = 1000, deg) {
    ...
    this.$refs.list.style.transition =  type === 'end' ? `transform ${time}ms cubic-bezier(0.19, 1, 0.22, 1)` : '';
    this.$refs.list.style.transform = `translate3d(0, ${translateY}px, 0)`;
}

Parent component part

In addition to the scrolling effect, we also have some business contents, such as gray mask, slide up pop-up, and work bar, which are handled by the parent component. Our business also involves multiple columns, so the parent component can split the props data and pass it to the child components, so that each child component can be independent of each other, listen to the event events of the child components and pass them to the outer layer.

3, Problems encountered

Our component is implemented based on px. In issues, we collected some problems encountered by some users. Here is a solution.

1. Using px2rem, the rotation of the roller deviates

Sometimes the value converted from px to rem will have deviation, and multiple decimal places will appear, resulting in the deviation between the rolling height and the actual converted height. We can solve this problem through the following configuration

The first kind: in Postcssrc JS configuration file, filter out those beginning with nutui

module.exports = ({ file }) => {
  return {
    plugins: [
        ...
        pxtorem({
            rootValue: rootValue,
            propList: ['*'],
            minPixelValue: 2,
            selectorBlackList: ['.nut'] // set up
        })
   }
}

The second: postcss px2rem exclude replaces postcss px2rem

npm uninstall postcss-px2rem
npm i postcss-px2rem-exclude -D
// In Postcssrc JS configuration
module.exports = ({ file }) => {
    return {
        plugins: [
            ...
            pxtorem({
                remUnit: rootValue,
                exclude: '/node_modules/@nutui/nutui/packages/picker'
            })
        ]
   }
}

2. Using lib flexible, the component is narrowed down

Our css is written when the data DPR is 1. If lib flexible is used, the page should be set

<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">

In the future, we will also consider solving the above problems from the code level.

summary

The above is the whole content of this article. It mainly introduces some design ideas and implementation principles of the Picker component. If you are interested in this component, you may wish to check and try it out. If you have any problems in use, you can issues We will answer and repair the questions as soon as possible. In the future, we will continue to optimize the components and visit NutUI component library , more components are waiting for you to find.

Tags: Vue.js Front-end

Posted by bubblocity on Wed, 01 Jun 2022 16:40:18 +0530