Implementation of a TV Remote Control Interaction Library for Webview Applications
This article details the design and implementation of a JavaScript library that enables TV remote navigation for webview‑based H5 pages, covering screen adaptation, focus management, scrolling logic, event handling, and practical usage within a TV‑side hardware product.
Introduction: The article shares the development of a TV‑side remote‑control interaction library for the "Future Box" hardware, which uses a webview (Chromium) to display H5 pages and requires navigation via a TV remote.
Screen adaptation: Since the TV screen is 1920×1080, the author uses a rem‑based scaling function 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(); to calculate font size based on design width and scale.
Core technical points: Determining scroll thresholds, smooth CSS3 transitions, element proximity calculations using getBoundingClientRect, handling layout differences, and simulating remote keys with keyboard events.
Remote object implementation: The library defines a constructor that stores focus area, groups, data, current element, index, directional element collections, callbacks, and scrolling containers. Sample constructor code is shown in this.focusArea = opt.allFocusParent || document; this.focusGroup = []; this.focusData = []; this.curDom = null; this.index = 0; this.leftRes = null; this.topRes = null; this.rightRes = null; this.bottomRes = null; this.key = "kindex"; this.canuse = true; this.highlightClass = opt.highlightClass; this.modifyDis = opt.modifyDis || 0; this.onconfirm = opt.onconfirm; this.onback = opt.onback; this.scrollContainer = opt.scrollContainer || document.documentElement; this.scrollObj = opt.scrollObj || document.getElementsByTagName("body")[0]; this.scrollBar = opt.scrollBar; this.scrollBarCtl = null; this.barMove = 0; this.lastPos = 0; this.stopPropagation = opt.stopPropagation || false; this.init(); .
Prototype methods: Event binding bindEvent(){ let _this = this; document.addEventListener('keydown', function(e) { if (!_this.canuse) { return false; } let keycode = e.keyCode; if (keycode == 37 || keycode == 21) { _this.leftFn(e); } else if (keycode == 38 || keycode == 19) { _this.upFn(e); } else if (keycode == 39 || keycode == 22) { _this.rightFn(e); } else if (keycode == 40 || keycode == 20) { _this.downFn(e); } else if (keycode == 13 || keycode == 23) { _this.enterFn(e); } else if (keycode == 27 || keycode == 90 || keycode == 4) { _this.backFn(e); } }, true); } maps keycodes to directional functions; navigation functions (leftFn, upFn, rightFn, downFn) compute the next index via getNextIndex . The upFn also demonstrates handing focus back to the TV side when no H5 element is above.
Core functions: init(){ window.scrollTo(0,0); this.scrollObj.style.transition = "all .3s ease"; this.setTranslateY(this.scrollObj,0); if (this.scrollBar) { /* scrollbar init */ } this.refresh(); this.highlight(); this.bindEvent(); } sets initial scroll position, enables GPU animation, creates custom scrollbars, and calls refresh() and highlight(); contentScroll(){ let tempST = window.getComputedStyle(this.scrollObj).transform.toString(); // ... calculations to keep focused element visible ... this.setTranslateY(this.scrollObj, -ScrollY); this.lastPos = Math.abs(ScrollY); } adjusts the scroll offset to keep the focused element visible; barScroll(scrollDirection, ScrollY){ /* updates custom scrollbar */ } updates the custom scrollbar; getNextIndex(direction){ /* calculates nearest focusable element */ } and helper methods (getNearDataVertical, getMinIndex) calculate the nearest focusable element.
Higher‑level methods: refresh(){ let _this = this, objs = _this.focusArea.querySelectorAll('*[autofocus]'); this.focusGroup = []; this.focusData = []; this.curDom = null; if (!objs.length) { console.warn('没有获取到焦点元素集合'); return false; } objs.forEach((item,i)=>{ item.setAttribute(this.key,i); this.focusGroup.push(item); this.focusData.push({txt:item.innerHTML.replace(/<.*?>/g,""), 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}); }); } rebuilds the focus group and data for elements with the autofocus attribute; 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(); } } applies the highlight class and triggers scrolling; enable() / disable() control event listening; go(index){ if(isNaN(index)){ console.log(index+'不是数字呢'); return false; } this.index = index; this.highlight(); } moves focus to a specific element.
Utility methods: 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))); } computes Euclidean distance; unique(arr){ for(let i=0;i removes duplicates; setTranslateY(obj,val){ obj.style.transform = "translate3d(0,"+val+"px,0)"; obj.style.webkitTransform = "translate3d(0,"+val+"px,0)"; } applies CSS transform; 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; } normalises numbers.
Usage example: An instance is created with let mainKB = new RController({highlightClass:'highlight', allFocusParent:oWrap, scrollObj:oIndex, modifyDis:oHeader.height()}); mainKB.onconfirm = function(curObj){ /* confirm callback */ }; mainKB.onback = function(){ /* back callback */ }; and callbacks for confirm and back are assigned.
Learning and reflection: The author notes the need for a reusable library as interaction logic grows, the challenges of debugging without actual hardware, and the importance of UI conventions to avoid unexpected issues.
Recruitment notice: The article ends with a brief hiring announcement for the technical team, inviting interested developers to apply.
TAL Education Technology
TAL Education is a technology-driven education company committed to the mission of 'making education better through love and technology'. The TAL technology team has always been dedicated to educational technology research and innovation. This is the external platform of the TAL technology team, sharing weekly curated technical articles and recruitment information.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.