Best practice of interactive class library of remote control on lightweight TV terminal

1, Introduction

Hello everyone, what I'm sharing today is the implementation of an interactive class library of TV remote control.

From the end of April to the middle of July last year, tal developed our first intelligent hardware, the future treasure box. The future treasure box is similar to millet box, with cartoon shape, which can be easily stuck on the top of the TV screen, connected with the TV with HD cable, and built-in the high-quality curriculum resources of tal. It is composed of lunch+ The App is composed of some web pages. The interaction mode is different from the H5 or web pages that you usually develop. In the future, the treasure box will use the remote control to interact with H5 pages (right, you heard right). There are four direction keys, i.e. up, down, left and right, OK key and return key. In the early days, there were only two simple pages in the TV end that used the remote control, order page and live course list. But the jump elements are all implemented by A tag, and only simple up and down scrolling and remote control interaction logic and business are coupled together, which can not be reused. However, with the new ideas of the operation partners to be verified on the TV end, web or H5 is A good solution, but there is no mature solution on the market, so we need to roll out one ourselves.

2, About adaptation

Before we start to talk about the implementation of the interactive class library, let's talk about the page adaptation in the TV end, because in the process of implementing the class library, we need to calculate the location relationship between the various elements. There is no big difference between the TV side page and the H5 page that you often write. The TV side App is developed by Android side, the container that carries the page is still webview, and the core is chromium (the version in our box is 65 at present). It is the same as the H5 application we developed in Android, but it is larger (1920 x 1080). Screen adaptation still uses the sharp tool of mobile terminal rem; the simple implementation of rem.

function initRem(opt) {
    let oWidth = document.documentElement.clientWidth,
        _designW = opt && opt.hasOwnProperty('designWidth') ? opt.designWidth : 1920,
        _scale = opt && opt.hasOwnProperty('nScale') ? opt.nScale : 100;
    document.documentElement.style.fontSize = oWidth / _designW * _scale + "px";
}
initRem();

When initRem() is called, opt is an object that can be passed but not. If the default design width 1920 is not transferred, the scale is 100 (easy to calculate when writing styles)

opt = {
    designWidth: 1920, // Design width
    nScale: 100	// Scale of px2rem
}

For example, the width of an element obtained from a 1920 wide design draft is 100px. When writing a style, width:1rem; is OK. To prevent the page from jumping, chain out this method and put it in the head tag.

3, Core technology points

  • Determine the critical point of the main content to scroll up or down
  • Use CSS3 to handle position offsets for smoother scrolling
  • Logic for filtering the closest elements horizontally and vertically
  • getBoundingClientRect().left gets the exact distance from the element to the left of the browser
  • Obtain the rolling distance of the current rolling content (the following two methods are selected according to the scene)
    • dom.style.transform
    • getComputedStyle(dom).transform
  • Screeding calculation deviation due to layout difference
  • All TV pages have a refresh button, so it will involve the transfer of focus control between native components and H5 components.
  • Use the direction key, enter key and ESC key of the computer to simulate the key of the remote control to realize the debugging of the TV end.

4, Remote control object realization

The remote control object is implemented in a hybrid way of classic constructor and prototype. Each instance of the method in the constructor is exclusive, and each instance of the method in the prototype is shared to save memory.

1. Remote control object overview

2. Constructor of remote control object

When writing HTML structure, add autofocus attribute to the element that needs to get focus, or add autofocus when using js to dynamically generate HTML structure, and select the element set we need through querySelectorAll("*[autofocus]").

As shown in the figure above, the constructor of the remote control object consists of the following core properties

this.focusArea = opt.allFocusParent || document; // The parent DOM of all the focus to be obtained in the current interface
this.focusGroup = []; // All DOM collections that need to get focus
this.focusData = []; // x,y, center point and index of all DOM that need to get focus

this.curDom = null; // Current DOM object
this.index = 0; // Currently highlighted index
this.leftRes = null; // The collection of buttons to the left of the current element
this.topRes = null; // Collection of buttons above the current element
this.rightRes = null; // Collection of buttons to the right of the current element
this.bottomRes = null; // Collection of buttons below the current element

this.key = "kindex"; // Custom properties for quick positioning of DOM
this.canuse = true; // Mark whether the current instance is available
this.highlightClass = opt.highlightClass; //Highlighted style
this.modifyDis = opt.modifyDis || 0; // Used to correct offset (mainly fixed head)
this.onconfirm = opt.onconfirm; // Confirmed callback
this.onback = opt.onback; // Callback returned

this.scrollContainer = opt.scrollContainer || document.documentElement || document.documentElement.body; // Scroll DOM object container
this.scrollObj = opt.scrollObj || document.getElementsByTagName("body")[0]; // DOM object to scroll
this.scrollBar = opt.scrollBar; // Custom scroll bar object 
this.scrollBarCtl = null;   // Scroll bar control slider
this.barMove = 0; // Scroll bar slider moving distance
this.lastPos = 0; //Record last location of content section
this.stopPropagation = opt.stopPropagation || false; // Whether it is allowed to call methods at the TV end when the up key is pressed and there is no focus element above

this.init();	// initialization

3. Prototye of remote control object

The prototype of the remote control object is divided into the following parts

I event monitoring

By listening to the keycode of the document, bind the related event callbacks to the four direction keys, the enter key and the back key of the remote control

// Binding event
bindEvent(){
    let _this = this;
    document.addEventListener('keydown', function(e) {

        if (!_this.canuse) {
            return false;
        }

        let keycode = e.keyCode;
        // 37, 38, 39, 40, 13, 27 90 are the keycode s on the computer keyboard
        // 21, 19, 22, 20, 23, 4 are the keycode s on the remote control
        if (keycode == 37 || keycode == 21) {
            // left
            _this.leftFn(e);
        } else if (keycode == 38 || keycode == 19) {
            // up
            _this.upFn(e);
        } else if (keycode == 39 || keycode == 22) {
            // right
            _this.rightFn(e);
        } else if (keycode == 40 || keycode == 20) {
            // down
            _this.downFn(e);
        } else if (keycode == 13 || keycode == 23) {
            // enter
            _this.enterFn(e);
        } else if (keycode == 27 ||  keycode == 90 ||  keycode == 4 ) { // 90 is the letter z; 4 is the return key of the remote control
            // 27 is esc, but esc must first execute system events, and then html events. This may cause clicking esc execution to return to the previous page to no effect.
            _this.backFn(e);
        }

    }, true);
}
// Left key callback
leftFn(e) {
    this.index = this.getNextIndex('left');
    this.highlight();
}
// Up key callback
upFn(e) {
    this.index = this.getNextIndex('up');
    if (!this.topRes.length && this.stopPropagation == false) {
        console.log('H5 There is no focus available above the current button, and the control of the focus will be transferred to TV End.');

        try {
            // Call methods on TV side
            qkJsCallAndroid.onTopFocusNone();
            console.log('H5 Successfully transferred the focus control of TV End.');
            this.dropFocus();
        } catch (e) {
            console.log(e);
        }
        return false;
    }
    this.highlight();
}
// Right click callback
rightFn(e) {
    this.index = this.getNextIndex('right');
    this.highlight();
}
// Down key callback
downFn(e) {
    this.index = this.getNextIndex('down');
    this.highlight();
}
// OK callback
enterFn(e) {
    if (this.onconfirm && typeof this.onconfirm == 'function') {
        // Performs a callback and passes in the current DOM object
        this.onconfirm(this.focusGroup[this.index]);
    }
}
// Fallback callback
backFn(e) {
    if (this.onback && typeof this.onback == 'function') {
        this.onback(this.focusGroup[this.index]);
    }
}

II core function
  • init() initialization function as the name implies. The initialization content is divided into the following parts:
init(){
    // Initial page to top
    window.scrollTo(0, 0);

    // Turn on GPU to perform animation
    this.scrollObj.style.transition = "all .3s ease";
    this.setTranslateY(this.scrollObj, 0);

    if (this.scrollBar) {
        // Initialize custom scroll bar
        let containerH = this.scrollContainer.clientHeight * 1, //Easy height for scrolling objects
            scrollObjH = this.scrollObj.clientHeight * 1,   // The height of the dynamic DOM object
            scrollBarH = this.scrollBar.clientHeight * 1;   // Height of scroll bar
        
        this.scrollBarCtl = this.scrollBar.firstElementChild;    // Scroll bar indicates block object

        if(scrollObjH < containerH){
            this.scrollBar.style.display = 'none'
        }else{
            this.scrollBar.style.display = 'block';
            this.scrollBarCtl.style.height = parseInt((scrollBarH * containerH) / this.scrollObj.clientHeight) + 'px';
            this.scrollBarCtl.style.transition = 'all .3s ease';
            this.scrollBarCtl.style.top = 0;
        }
    }

    this.refresh();  // Traverse the elements with autofocus in the corresponding DOM structure
    this.highlight(); // First selected by default
    this.bindEvent(); // Bind remote control events
}

  • contentScroll() content main body scrolling logic, mainly simulates the up and down scrolling of the page. The figure of the critical point for the main content to scroll up or down is as follows:

The code logic is as follows:

// Content scrolling logic
contentScroll(){
    let tempST = window.getComputedStyle(this.scrollObj).transform.toString();

    if (tempST == 'none' || tempST == '0') {
        tempST = 0;
    } else {
        tempST = tempST.substring(7);
        tempST = tempST.substring(0, tempST.length - 1).split(',')[5];
    }

    // After highlighting, judge whether the focus element is in the visible area
    let scrollObjST = Math.abs(tempST), // The distance the scrolling object moves up
        containerH = this.scrollContainer.clientHeight, //Easy height for scrolling objects
        curObjH = this.curDom.offsetHeight, // The height of the currently obtained focus object
        curObjOffsetTop = this.curDom.offsetTop, // 
        ScrollY = 0; // Distance to scroll in y direction

    if ((curObjOffsetTop + curObjH) > (containerH + scrollObjST)) {
        // console.log('not visible below browser ');
        ScrollY = curObjOffsetTop + curObjH * 1.4 - containerH;
        if (Math.abs(ScrollY) > (this.scrollObj.clientHeight - this.scrollContainer.clientHeight)) {
            ScrollY = this.scrollObj.clientHeight - this.scrollContainer.clientHeight;
        }
        // Optimize the distance from the top,
        ScrollY = parseInt(ScrollY) + curObjH * 0.2;
        // The calculation method of barMove: ScrollY / (scrollObjH - containerH) = barMove / (this.scrollBar.clientHeight - scrollBarCtl.clientHeight)
        
        // Custom scroll bar logic
        if (this.scrollBar) {
            this.barScroll('up',ScrollY)
        }
        this.setTranslateY(this.scrollObj, -ScrollY);
        this.lastPos = Math.abs(ScrollY);
    }

    if (scrollObjST > 0 && (scrollObjST + this.modifyDis) > curObjOffsetTop) {
        // console.log('not visible above browser ');
        ScrollY = curObjOffsetTop - curObjH * 0.6 - this.modifyDis;
        if (ScrollY < 0) {
            ScrollY = 0;
        }

        // Custom scroll bar logic 
        if (this.scrollBar) {
            this.barScroll('down',ScrollY)
        }

        this.setTranslateY(this.scrollObj, -ScrollY);
        this.lastPos = Math.abs(ScrollY);
    }
}

  • barScroll(): scroll bar logic. In init(), the actual height of the scroll bar slider is dynamically calculated by the ratio of the height of the actual content and the visible area. In the processing logic of content scrolling, if the scroll bar needs to be displayed, the scroll bar logic is executed.
// Scroll bar logic
barScroll(scrollDirection, ScrollY) {
    let containerH = parseInt(this.scrollContainer.clientHeight);
    
    if(scrollDirection == 'up'){
    	// scrollDirection content is about to scroll up
        this.barMove = 0; // Fix the position of the scroll bar slider
        this.barMove += parseInt(ScrollY * (this.scrollBar.clientHeight - this.scrollBarCtl.clientHeight) / (this.scrollObj.clientHeight - containerH));
        if (this.barMove > (this.scrollBar.clientHeight - this.scrollBarCtl.clientHeight)) {
            this.barMove = this.scrollBar.clientHeight - this.scrollBarCtl.clientHeight
        }
        this.setTranslateY(this.scrollBarCtl, this.barMove);
    }else if(scrollDirection == 'down'){
    	// scrollDirection content is about to scroll down
        this.barMove -= parseInt(Math.abs(ScrollY - this.lastPos) * (this.scrollBar.clientHeight - this.scrollBarCtl.clientHeight) / (this.scrollObj.clientHeight - containerH));
        if (this.barMove <= 5) {
            this.barMove = 0
        }
        this.setTranslateY(this.scrollBarCtl, this.barMove);
    }
}

  • getNextIndex() gets the index of the next element. The acquisition logic of horizontal and vertical is slightly different. In vertical direction, the buttons in each row should get the focus in turn from top to bottom, so the filtering logic is to find the closest element in vertical direction with the smallest distance between centers. In the horizontal direction, first filter the elements that are on the same horizontal line with the current focus and have the smallest center point spacing.
// The logic of getting the index of the next element is different between horizontal and vertical
getNextIndex(direction) {
    let theNearest = null,
        allResult = [], // Get all buttons in corresponding direction
        curParam = this.focusData[this.index]; // Parameters corresponding to the current element

    if (direction == 'left') {
        // Filter out all the elements that need to be highlighted in the left horizontal direction of the currently highlighted element and store them in allResult
        this.focusData.forEach(item => {
            if (item.cx < curParam.x && item.cy > curParam.y && item.cy < (curParam.y + curParam.h)) {
                allResult.push(item);
            }
        });
        if (allResult.length > 0) {
            this.leftRes = allResult;
            theNearest = this.leftRes[this.getMinIndex(this.leftRes)];
        } 
    } else if (direction == 'up') {
        // Filter out all the elements that need to be highlighted under the current highlighted element and store them in allResult
        this.focusData.forEach(item => {
            if (item.cy < curParam.cy) {
                allResult.push(item);
            }
        });
        
        theNearest = this.getNearDataVertical(allResult, 'cy', 'max', 'up');
    } else if (direction == 'down') {
        // Filter out all the elements that need to be highlighted under the current highlighted element and store them in allResult
        this.focusData.forEach(item => {
            if (item.cy > curParam.cy) {
                allResult.push(item);
            }
        });
        theNearest = this.getNearDataVertical(allResult, 'cy', 'min');
    } else if (direction == 'right') {
        // Filter out all the elements that need to be highlighted in the horizontal direction to the right of the currently highlighted element and store them in allResult
        this.focusData.forEach(item => {
            if (item.cx > (curParam.x*1 + curParam.w*1) && item.cy > curParam.y && item.cy < (curParam.y + curParam.h)) {
                allResult.push(item);
            }
        });
        if (allResult.length > 0) {
            this.rightRes = allResult;
            theNearest = this.rightRes[this.getMinIndex(this.rightRes)];
        } 
    }

    // The nearest is an element of focusData
    if (theNearest) {
        return theNearest.index;
    } else {
        return this.index;
    }
}

// Get the closest element data in the vertical direction
getNearDataVertical(arr, state, direction){
    let tempArr = [],  // Temporary array
        resArr = [],    // Result
        tempVal = 0;    // Median

    arr.forEach(item => {
        tempArr.push(item.cy);
    })
    tempArr = this.unique(tempArr);

    tempVal = Math[state].apply(null, tempArr);

    arr.forEach(item => {
        if(item.cy == tempVal){
            resArr.push(item);
        }
    });
    if(direction && direction == 'up'){
        this.topRes = resArr;
    }
    return resArr[this.getMinIndex(resArr)];
}

// Returns the index closest to curobj 
getMinIndex(arr) {
    let arrDis = [],
        curPoint = this.focusData[this.index];
    arr.forEach(item => {
        arrDis.push(this.getDis(item, curPoint));
    })
    let minval = Math.min.apply(null, arrDis);
    return arrDis.indexOf(minval);
}

III higher order method
  • refresh() is mainly used to solve the problem that the layout dom loses the focus getting Bug after refreshing. In the function, you need to process the binding event for all elements with the attribute of audofocus, and collect some data to prepare for the next filtering button.
// Refresh
refresh() {
    let _this = this,
       objs = _this.focusArea.querySelectorAll('*[autofocus]');

   this.focusGroup = []; // All DOM collections that need to get focus
   this.focusData = []; // x,y, center point and index of all DOM that need to get focus
   this.curDom = null; // Current DOM object

   if (!objs.length) {
       console.warn('Focus element collection not obtained');
       return false;
   }

   objs.forEach((item, i) => {
       item.setAttribute(this.key, i);
       this.focusGroup.push(item);
       this.focusData.push({
           txt: item.innerHTML.replace(/<.*?>/g,"").replace(/[\r\n]/g,"").replace(/[ ]/g,"").trim(),
           w: parseInt(item.offsetWidth),
           h: parseInt(item.offsetHeight),
           x: parseInt(item.getBoundingClientRect().left),
           y: this.formatInt(parseInt(item.getBoundingClientRect().top)),
           cx: this.formatInt(parseInt(item.getBoundingClientRect().left) + parseInt(item.offsetWidth / 2)),
           cy: this.formatInt(parseInt(item.getBoundingClientRect().top) + parseInt(item.offsetHeight / 2)),
           index: i
       });
   });
}

  • highlight() locates the current element and gives the highlight style
// Highlight
highlight(){
    this.focusGroup.forEach(item => {
        item.classList.remove(this.highlightClass);
    });
    this.curDom = this.focusGroup[this.index];

    if(this.curDom){
        this.curDom.classList.add(this.highlightClass);
        this.contentScroll();
    }
}

  • disable() / enable() is mainly used to disable and start the scrolling of the main content when the bomb layer appears. When canuse is false, pause event listening on the remote control instance. See the logic of event monitoring above for details
// Prohibit
disable() {
    this.canuse = false;
}
// Enable
enable() {
    this.canuse = true;
}

  • dropFocus() / getFocus() lose focus, get focus
// Lose focus
dropFocus() {
    this.focusGroup.forEach(item => {
        item.classList.remove(this.highlightClass);
    });
}
// Get focus todo 
getFocus() {
    this.highlight();
}

  • go(index) to the desired element
go(index){
    if (index == isNaN) {
        console.log(index + 'It's not a number');
        return false;
    }
    this.index = index;
    this.highlight();
}

IV tool method
// Returns the shortest distance between two points
getDis(p1, p2) {
    let dx = Math.abs(p1.cx - p2.cx),
        dy = Math.abs(p1.cy - p2.cy);         
    return parseInt(Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)));
}
// Array de duplication
unique(arr) {
    for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[i] == arr[j]) {         //The first is equivalent to the second, and the splice method deletes the second
                arr.splice(j, 1);
                j--;
            }
        }
    }
    return arr;
}
// Set translateY
setTranslateY(obj, val){
    obj.style.transform = "translate3d(0," + val + "px,0)";
    obj.style.webkitTransform = "translate3d(0," + val + "px,0)";
}

// Format data to an integral multiple of 10 to smooth out minor differences in layout
formatInt(num, prec = 1){
    const len = String(num).length;
    if (len <= prec) { return num }; 
    
    const mult = Math.pow(10, prec);
    return Math.floor(num / mult) * mult;
}

5, Call of remote control object

let mainKB = new RController({
    highlightClass: 'highlight',   // Highlight style
    allFocusParent: oWrap,	// All the parent DOM objects that need to get focus
    scrollObj: oIndex, // Scrolling DOM objects
    scrollContainer: '', // Scroll DOM object container
    modifyDis: oHeader.height()// Used to correct offset (mainly fixed head)
});

mainKB.onfirm = function(curObj){
	// The curObj returned by the callback of the enter key is a native DOM object, which collects the third-party framework or class library to implement business logic such as jump / ajax.
}
mainKB.onback = function(){
	// Callback by pressing the return key
}


6, Learning and reflection

  1. For the first time, the logic of remote control is relatively simple. However, the logic of this time is relatively complex, and the future attempt is expected to be more and more complex. Therefore, when similar logic appears more than twice, we need to consider abstracting the function into the basic library, which not only facilitates ourselves, precipitates the technology, but also facilitates everyone and improves the development efficiency.

  2. There is no box or remote control in the debugging of TV terminal, and it completely depends on browser. There are more ways than problems.

  3. Although the interaction class library will not limit the UI too much, it is better to have certain specifications for the UI, so as to avoid some inexplicable problems.

end

recruitment information

Good future technology team is in the direction of advanced research and development engineer, such as hot test, backstage, operation and maintenance, client, etc., you can click on the "good future technology" official account "technology Recruitment" column for details. Welcome interested partners to join us!

Maybe you want to see it

Science behind "examination": Theory and model in educational measurement (IRT)

Help education with technology

Do you want to understand the architecture evolution process of an off-site multi school platform? Let me tell you!

Design and implementation of the game system for Moby show (based on Egret+DragonBones keel animation)

How to implement a pager plug-in

There is no respite for the outbreak and war of production and research personnel

Tags: Front-end Android Attribute Mobile css3

Posted on Fri, 24 Apr 2020 03:07:32 -0700 by nailzfan