preface

Today’s topic is the design and implementation of the NutUI Picker component. The Picker component is a NutUI Picker component that displays a series of value sets that the user can scroll to select from, or multiple series of value sets that the user can select from separately. Let’s look at a rendering of what the component does.

Speaking of NutUI, for those of you who may not know much about it, let’s give you a brief introduction. NutUI is a set of JINGdong style mobile Vue component library, development and service for mobile Web interface enterprise front, middle and back products. With NutUI, you can quickly build unified pages to improve development efficiency. At present, there are more than 50 components, which are widely used in various mobile terminal businesses of JD.

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

  • Why encapsulate components
  • How the NutUI Picker component works
  • Problems encountered

Why encapsulate components

When the business reaches a certain scale, it will encounter many similar functional interfaces, and every time it is redeveloped, the development efficiency will be affected. Moreover, some problems may lurk in these similar codes. Once exposed, we need to spend a lot of time to deal with the same codes in the business. If we rationalize the same code by pulling it apart, packaging components, and calling it multiple times, we see a qualitative improvement in development efficiency.

Here’s a look at the benefits of wrapping components:

Packaging components can not only make collaborative development efficient and standardized, but also bring more convenience for subsequent business expansion with the componentized front-end development mode.

Some people say, “It’s like reinventing the wheel.” When it comes to wheels, most people associate it with the famous saying “Don’t reinventing the wheel.” But what a lot of people don’t know is that Stop Trying to Reinvent the Wheel, which really means don’t Reinvent the Wheel. The previous wheel may not meet all of our development needs, but we can build on it and improve it, so as to get a better wheel, through this gradual process, to meet our needs.

2. NutUI Picker component implementation principle

This component is relatively common in everyday business requirements. It can not only carry simple TAB functions, but also meet the more tedious date and time selection, or cascading address selection function. Based on the Date and time components of the Picker component, we also have packages that interested people can access to the NutUI component library to view.

From the introduction to this article, we have seen briefly what the Picker component does, which selects an item in a selection set through a three-dimensional rotation similar to a scroll wheel.

Let’s take a look at the component source directory structure:

Let’s focus on the last three files. Based on the proximity principle, we put related files in the same directory. Based on the single responsibility principle, we granulated components to keep them as simple and universal as possible. The picker component is divided into the parent component picker.vue and the child component picker-slot.vue, which is only responsible for the roller interaction. The parent component handles the business class logic.

Child component roller section

1. Take a look at the dom division of labor

<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>
Copy the code
  • The nut – picker – indicator: line
  • Nut-picker-content: Highlight the selected area
  • Nut-picker-roller: a roller area

Don’t want to see the code? “Mistress, picture above!”

2. CSS

Set the Nut-Picker-Indicator at the highest level to avoid being obscured

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

The 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; . }}Copy the code

Transform-style :preserve-3d; Is essential. In general, this property applies to the parent element of the 3D transformation, the stage element. This gives the element a 3D attribute effect. In the 3D world of CSS, by default, we can see the elements behind us. To be practical, we often make the elements behind us invisible, so we set the child element backface-visibility: hidden; Note that the transform-style:preserve-3d property does not prevent overflow of child elements. If overflow: hidden is set, the transform-style:preserve-3d property is invalid.

We simulate the rotation of the wheel to realize the interaction of the components, using a side view to get a more intuitive view.

So let’s see how that works.

First of all, a sphere needs to be simulated, and each item of the selection set (hereinafter referred to as “wheel item”) is set to position: Absolute, sharing the same center point, namely the center of the sphere, and then stacked here successively.

To review the basics, the translate3D () function moves an element in three dimensions. This deformation is characterized by using the coordinates of a three-dimensional vector to define how much the element moves in each direction. When the z axis value, the greater the elements also closer to the viewer, we set the z axis to the ends of the roller items reach the surface of the sphere, the size of the z axis, equivalent to the radius of the sphere, and because we set the height of the viewing area is 260, so set the radius is 104, if the radius is too small, we need to wear high power magnifying glass to look for roller, if the radius is too large, So the wheel term goes behind our head… Can’t let eyes in the back of the head such a terrible thing happen! Distance creates beauty, so keeping a proper distance (80%) is the most beautiful.

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

At this point, we see that all the rollers go from being collectively stacked at the center of the sphere to being stacked at two points of the sphere, and we need to lay them out according to the circumference. The rotate3D () property is used to rotate the wheel around the X-axis, so rotate3D (1, 0, 0, a) is the X-axis. A is an Angle value that specifies the Angle by which the element rotates in 3D space. If the value is positive, the element rotates clockwise, and the element rotates counterclockwise. So how do we set this Angle? We can use a central Angle formula to infer that the degree of the central Angle is equal to the degree of the arc it is opposite. Our radius is 104, and the arc length is 36 (our preset display area), so we can round the Angle a to 20. Is there a feeling of being confused? Let’s get a more intuitive understanding through a picture.

Using the above analysis, let’s dynamically set the final position of the wheel item.

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

It should be noted that the number of scroll items may be very large, and the possibility of more than one circle exists greatly. However, we can neither show the specified number to the user at the same time, nor show all of them to cause overlapping problems. At this point, we need to hide the excess, we know that the Angle a is 20 degrees, and the circumference of the circle is 360 degrees, so we can show up to 18 points, and we use the current center as the base point, showing 8 points in the front and 9 points in the back.

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

3. Add events

Finally, we add the swipe event by getting the DOM element associated with the Vue instance and setting the TouchStart, TouchMove, and TouchEnd events. 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 end point of the scroll, calculate the difference, and dynamically set the scroll distance and scroll Angle of the outermost element of the scroll wheel. It is necessary to correct the rolling distance when rolling to ensure that the final rolling distance is a multiple of lineSpacing (height of roller item 36).

We also added an increase in elasticity, allowing the TouchMove to scroll beyond the scroll range, and then correcting the position of the first and last items in the TouchEnd event.

Let’s look at the implementation.

setMove(move, type, time) { let updateMove = move + this.transformY; If (type === 'end') {// if (updateMove > 0) {updateMove = 0; } if (updateMove < -(this.listData.length - 1) * this.lineSpacing) { updateMove = -(this.listData.length - 1) * this.lineSpacing; } // Set the rolling length to a 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 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); }},Copy the code

In TouchEnd, a transition “easing function” was added to the wheel parent element to simulate inertial scrolling.

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}) `;
}
Copy the code

Through the above content, our wheel effect has been basically formed. But we also want something like ios where the time picker highlights the current area. How do we do that?

We tried the following three approaches.

First, consider changing the font when the scroll item stays in the highlighted selected area, but practice found that the font can only be changed at the end of the scroll, not during the scroll, which is not a friendly experience.

Second, can you skillfully use CSS, using background gradient and background-size with complete gradient, using a mask to achieve it?

.nut-picker-mask{
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-image: linear-gradient(180 deg, hsla (% 0, 0, 100%, 9),hsla(% 0, 0, 100%, 6)),linear-gradient(0 deg, 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;
}
Copy the code

Let’s make the background yellow so we can see what it looks like.

It feels all right. Is that it?

We simulated everything was normal on the PC side, but weird pictures appeared on the real computer. When sliding up and popping up, the mask would delay the display, affecting the experience effect. Only when the up-slide transition effect is prohibited can it be displayed normally. It is impossible to remove the slippage effect, so we have to consider other methods.

Third, is it possible to set up a secondary scroll, the highlight area mentioned above, on top of the scroll wheel, each element in the highlight area is equal to the height of the viewable area, and when the scroll wheel slides, the list elements within the highlight area slide along with it.

Practice has proved that this method can avoid the drawbacks of the above two methods and perfectly solve our needs. Let’s see how it works.

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

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

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)`;
}
Copy the code

Parent component part

In addition to the scrolling effect, we have some gray masks, slide-up pop-ups, work bars and other business content that we leave to the parent component. In our business, multiple columns are also involved, so the parent component can split the props data to the child components, so that each child component is independent of each other, and listens for the event event of the child component, which is passed to the outer component.

3. Problems encountered

Our component is based on PX to achieve, in issues, collected some users encountered some problems, here provides solutions.

1. When pX2REM is used, there is deviation in the rotation of the roller

Because px to REM sometimes turn out the value of deviation, and there are many decimal places, resulting in the height of scrolling and the actual height of conversion deviation, we can solve the following configuration

First: filter out nutui in the. Postcsrc. Js configuration file

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

Postcss-px2rem-exclude replaces postCSs-px2REM

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

2, Using lib-flexible, components are minified

Our CSS was written when data-DPR was 1, and if lib-flexible was used, the page would have to be set

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

We’ll consider addressing the above issues at the code level later.

conclusion

Above is all content of this article, mainly introduced the some design ideas and implementation principle of Picker component, if you are interested in this component, might as well to see and try to use if you have any question, can ask questions on the issues, we will answer as soon as possible and repair, we will follow-up to continuously optimize the iteration of the component, Visit the NutUI component library for more to discover.