简介
tv-focusable 是适用于在 TV 端进行网页开发时管理焦点移动的框架,以简洁的 Api 让前端网页开发就像 android 开发一样自动管理焦点。
更多案例展示
vue-tv-focusable
react-tv-focusable
安装
npm i ng-tv-focusable
如何使用
app.module.ts中添加如下代码
// app.module.ts
import { TvFocusableModule } from 'ng-tv-focusable';
@NgModule({
declarations: [...],
imports: [
...,
TvFocusableModule
],
...
bootstrap: [AppComponent]
})
设置元素可获取焦点
<div [ng-focusable]>可获取焦点的元素</div>
<div>不可获取焦点的元素</div>
跟 android 很相似了吧~
给非 android 开发的同学普及一下,在进行 android 的 TV 端开发时,系统是会自动给 focusable=true 的元素分发焦点的,例如:
<TextView
android:id="@+id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="true" />
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="true" />
假设这两个元素是左右排列的,当焦点在前面一个 text1 上时,按遥控器的右键,焦点会自动跑到 text2 上。
现在在网页标签上设置 [ng-focusable] 就相当于 android 上设置了 android:focusable=“true”,当用户按遥控器时,该库会像 android 那样自动把焦点移动到下一个焦点上,无需开发者处理。
别急,现在还看不到焦点效果,因为你还没设置聚焦的样式呢。
设置焦点的样式
聚焦的元素会被加上一个 class="focus"
(你可以自己定义 className,默认为 focus),现在你应该知道要做什么了,给 focus 设置个独特的样式就好了。
例如这样:
<div class="demo">
<span
*ngFor='let in of counter(60) ;let i = index'
class="span" [ng-focusable]>
{{i+1}}
</span>
</div>
...
.focus {
transform: scale(1.1);
border: 2px solid red;
box-shadow: 0 0 20px red;
}
好了,这样就设置完了,现在你按方向键时焦点就会自动移动到下一个目标元素上了,很简洁吧~
指定焦点移到到某一个元素上
上面的演示中,最开始时第一个元素获取了焦点,实际上你在使用时,库并不会在页面渲染完成后主动帮你聚焦到第一个或某个元素上,这个由你自己来指定似乎更加合适,你可以用下面的方法来指定焦点移动到某个元素上:
import { $tv } from 'ng-tv-focusable';
$tv.requestFocus(Element,bool); // Element 必填; bool 非必填,默认为true
关于第二个参数,设置聚焦时是否需要滚动动画,默认 true 带动画,如果指定的聚焦元素不在窗口可视区域内或未完全显示,聚焦后就会被滚动到可视区域内,有些场景下你可能并不想要动画,例如类似 “回到顶部” 的场景,就需要用到这个了,试一下看看吧~
例如:
<div class="box">
<div class="item">s</div>
</div>
...
import { $tv } from 'ng-tv-focusable';
$tv.requestFocus($tv.getElementByPath('//div[@class="box"]/div[1]'));
至于如何获取 Element 就不再展开了,ng-tv-focus提供了 xpath 获取Element:
$tv.requestFocus($tv.getElementByPath('//div[@class="demo"]/div[2]'));// 让demo下的第2个div 聚焦
一般 TV 端的页面在刚加载完成时,产品都会要求让某一个元素聚焦的,这时你就用上这条 api 了。
注意: 进入页面时想让某个元素聚焦,要在 ngAfterViewInit 回调中进行,否则会因页面未加载完全而导致焦点移动并不如你所愿。
XPath 获取 DOM—getElementByPath(string)
ng-tv-focusable 提供了 xPath 的方式获取
<body>
<div class="content">
<span class="item1">1</span>
<span class="item2">2</span>
<span class="item3">3</span>
</div>
</body>
$tv.getElementByPath('//div[@class="content"]/span[2]');
$tv.getElementByPath($tv.readXPath(document.querySelector('.item2')));
//结果: <span class="item2">2</span>
根据 DOM 反向获取 XPath 路径 —readXPath(el)
ng-tv-focusable 根据 DOM 反向获取 XPath 路径
<body>
<div class="content">
<span class="item1">1</span>
<span class="item2">2</span>
<span class="item3">3</span>
</div>
</body>
$tv.readXPath(document.querySelector('.item2'))
$tv.readXPath($tv.getElementByPath('//div[@class="content"]/span[2]');)
//结果: "/html/body/div[1]/span[2]"
自定义焦点移动
框架有自己的一套找焦点算法,但有时产品要求你在某个元素上按遥控器右方向键时,让屏幕左下角的某个该死的元素聚焦,那你就要根据产品需求文档来自定义焦点移动了。
可以使用 (up),(right),(down),(left) 这几个属性来指定按遥控器对应方向键时调用的方法,然后自己在方法里通过 next 或 requestFocus(1.x只支持此方法) 让目标元素聚焦;
1.x写法
如果你在自己的方法里调用了 requestFocus 方法,该库将不再执行默认的焦点移动操作,否则依然帮你执行默认的焦点移动。
示例:
<div class="wrapper">
<div class="focus-item" [ng-focusable] *ngFor='let in of counter(100) ;let i = index' (down)="down(i)">
</div>
...
down(index: number) {
if (index === 2) { //在第2个div上按下方向键时,焦点直接移动到第30个div上
$tv.requestFocus($tv.getElementByPath('//div[@class="wrapper"]/div[30]'));
}
}
2.x写法
只要你在标签上定义了方向键监听例如(left),那么在此标签上按左键时该库将不会再执行默认的向左焦点移动操作,你可以通过如下用法来移动焦点:
<div class="wrapper">
<div class="focus-item" [ng-focusable] *ngFor='let in of counter(100) ;let i = index' (left)="left(i)">
</div>
...
left(index: number) {
if (index === 2) {
//和requestFocus功能是一样的,推荐使用next
$tv.next($tv.getElementByPath('//div[@class="wrapper"]/div[30]'))
//或者如果你想在处理一些事情之后再执行库的默认移动
setTimeout(() => {
$tv.next("left")
}, 500);
}
}
推荐使用 2.x 的监听,可同时支持同步和异步流程,1.x只支持同步流程。
表单控件,按确定键进行填写
// formAutofocus:默认true
$tv.formAutofocus=false;// 不可以输入
$tv.formAutofocus=true;// 可以输入
好啦,到这里应该能满足基本所有的 TV 焦点需求了。
你还在看???
好吧,或许你还有一些别的需求~
其它
初始化
可以在全局或某个页面上配置属性
// 初始化配置
// 全局配置可在app.component.ts中
// 1.使用init同时配置多个属性
import { $tv } from 'ng-tv-focusable';
$tv.init({
focusClassName: "on-focus", // 聚焦元素的className (默认focus)
KEYS: {
KEY_LEFT: [37, 21],
KEY_UP: [38, 19],
KEY_RIGHT: [39, 22],
KEY_DOWN: [40, 20],
KEY_ENTER: [13, 23]
}, // 自定义键值
longPressTime: 800, // 长按响应时间(单位:毫秒),默认500ms
distanceToCenter: false // 使焦点始终在可视范围的中间部分,默认false
});
// 2.单独配置初始化
$tv.KEYS= {
KEY_LEFT: [37, 21],
KEY_UP: [38, 19],
KEY_RIGHT: [39, 22],
KEY_DOWN: [40, 20],
KEY_ENTER: [13, 23]
};
$tv.longPressTime= 800;
**警告:**为了不影响通用配置,如果对某页面进行了一些特殊配置,请记住在销毁页面时重置它
ngAfterViewInit(){
$tv.findFocusType=0 ;
}
ngOnDestroy() {
$tv.resetFindFocusType(); //$tv.findFocusType = 1;
}
上面只是列举了几个常用且一般是设置成整个项目所有页面通用的属性,可在文档最下方查看所有可配置属性,如果不需要定制属性,无须初始化。
通常情况下,默认属性就能满足大部分项目的需求。
下面是几个常用属性使用场景的举例说明:
自定义焦点 class(focusClassName)
你接手了别的组的项目,他们已经把 focus
这个词作为普通 className 给用了,而且很多地方都用到了,你一定不愿意把他们的代码里的 focus 改成 focus1,focus2 什么的,以使你可以正常使用该库的 focus 这个名字作为焦点样式。
那就通过初始化里的 focusClassName 属性重新定义一下该库的焦点样式名吧。
自定义键值(KEYS-配置上下左右,enter键值)
不同厂家的遥控器键值可能不一样,有些并未按照 android 键值来实现,而且产品可能要求同时支持遥控器和键盘操作,这时你就需要指定上下左右方向键值了,例如上面配置的 KEY_LEFT: [37, 21] 就是支持键盘上的左箭头键和遥控器左方向键。
自定义找焦点方式(findFocusType,initDis)
findFocusType=1,即默认找最近焦点方式,用默认方式时实际上用不到 initDis 参数
findFocusType=0,initDis = 49
initDis 为何是 49?只要下一个 div 的横向中心线和当前焦点元素的中心线距离大于 initDis 值,就认为它和当前焦点不在同一水平线上,现在 div2 的横向中心线比 div1 高了 50px,所以如果你不想让用户在 div1 上按右方向键时焦点跑到 div2 上,就可以设置找焦点方式为直线模式,并通过 initDis 来控制偏差范围。
边缘距离(offsetDistance)
到达边缘的时候,给焦点和边缘一个距离.
当某个未完全显示在窗口可视区域内的元素聚焦时,该库会自动帮你将这个元素滚动到窗口内完全显示,不过默认状态是贴着边缘的,如果你想让它离这个边缘有一个距离,就可以通过这个属性实现了。
$tv.offsetDistance = 50 // 完全显示出来后再距离边缘50像素
如图:
局部滚动(setScrollEl/resetScrollEl)
TV 端网页大多是整个页面滚动,滚动条在浏览器窗口上,如果你的滚动内容是在整个页面的局部某一块,需要指定一下滚动容器。
setScrollEl(Element): 设置滚动的el
resetScrollEl(): 重置el,即滚动浏览器的滚动条
举个例子
<div class="demo">
<div class="wrapper">
<div class="focus-item" [ng-focusable] *ngFor='let in of counter(300);let i = index' :key="index">
{{ i+1}}
</div>
</div>
</div>
<script>
...
ngAfterViewInit() {
$tv.scrollEl=$tv.getElementByPath('//div[@class="demo"]');
}
ngOnDestroy() {
// To avoid affecting the global configuration, reset the page when destroying it
$tv.resetScrollEl();
}
...
</script>
<style>
.demo {
background:#ccc; margin: 0 auto; width: 400px; height: 400px;
position: relative; padding: 20px; overflow: hidden;
.wrapper { width: 800px; }
.focus-item {...}
}
</style>
自定义事件
监听焦点状态
(onFocus):获得焦点
(onBlur):失去焦点
<div class="focus-item" [ng-focusable] *ngFor='let in of counter(105);let i = index' (down)="down(i)"
(onFocus)="focus(i,$event)" (onBlur)="blur(i)">
{{ i+1}}
</div>
...
focus(index: number, event: any) {
console.log(event.detail.el); //当前元素
console.log('focus:' + index);
}
blur(index: number) {
console.log('blur:' + index);
}
不需要获取当前元素就不用传 $event 了。
点击
@click
按遥控器 OK 键(相当于手机上手指点击按钮,PC 上鼠标点击按钮)
<div class="focus-item" v-focusable (click)="click()">button</div>
...
click() {
console.log('click');
}
长按
按下遥控器 OK 键超过 500ms 会触发长按事件,可通过 $tv.longPressTime 自定义这个时间
(longPress)
<div class="focus-item" [ng-focusable] (longPress)="longPress()">11</div>
...
longPress() {
console.log('longPress');
}
方向按键
left,right,up,down
TV 端依靠遥控器按上下左右方向键来操作界面,可自定义方向事件监听,例如监听右键按键事件
<div class="focus-item" v-focusable (right)="right()">11</div>
...
right() {
console.log('right');
}
设置属性/重置属性
// 重置成插件默认值,reset+属性名(),例如
$tv.resetScrollEl();
$tv.resetFocusClassName();
//清除所有配置并恢复到插件的默认配置
$tv.reset()
...
// 独立配置init里面的每一项,$tv.属性名=xx, 如下:
$tv.focusClassName = XX;
$tv.offsetDistance = XX;
其它可能用到的方法
元素聚焦状态的全局监听- setOnFocusChangeListener
$tv.setOnFocusChangeListener((element, focus)=>{
console.log("焦点状态改变的元素:" + element, "聚焦或失去焦点:" + focus)
})
让页面滚动到某个元素的位置
$tv.scrollTo({targetEl: document.querySelector("#page1")})
参数说明:
scrollEl:滚动区域,默认为 document,
targetEl:需要滚动到哪个元素,
isCenter:targetEl 是否滚动到屏幕居中的位置,
offset:距离滚动边界上下左右的偏移,
smooth:是否开启滚动动画,
duration:动画耗时,默认200毫秒
所有可配置项
属性
name | 描述 | 默认值 |
---|---|---|
itemAttrname | 可聚焦的属性名 $tv.itemAttrname= “myFocusable”; | focusable |
focusClassName | 指定焦点元素类名 $tv.focusClassName = “myFocus”; | focus |
findFocusType | 移动类型,1:就近查找 0:直线查找 $tv.findFocusType = 1; | 1 |
initDis | 差值 $tv.initDis = 20; | 20 |
KEYS | 上下左右的键值 $tv.KEYS = {…}; | { KEY_LEFT: [37, 21], KEY_UP: [38, 19], KEY_RIGHT: [39, 22], KEY_DOWN: [40, 20], KEY_ENTER:[13, 23] } |
offsetDistance | 边缘距离(单位px) $tv.offsetDistance = 50; | 50 |
longPressTime | 长按响应时间(单位毫秒) $tv.longPressTime = 500; | 500 |
scrollEl | 设置可以滚动的el $tv.scrollEl = ; | null |
limitingEl | 整个界面,只有limitingEl内的焦点可以聚焦 $tv.limitingEl = ; | null |
distanceToCenter | 焦点是否在滚动区域保持居中显示 $tv.distanceToCenter = true; | false |
formAutofocus | 表单控件是否可以输入或者编辑 $tv.formAutofocus= false; | true |
smoothTime | 滚动的动画时间 $tv.smoothTime= 200; | 200 |
spacingTime | 滚动的动画帧数(数值越小,滚动动画帧数越多,动画更细腻,也会更耗性能) $tv.spacingTime = 20; | 20 |
scrollSpeedX | 控制水平方向按键速度(单位毫秒),例如200ms内只执行一次按键事件 $tv.scrollSpeedX = 200; | 0 |
scrollSpeedY | 控制垂直方向按键速度(单位毫秒) $tv.scrollSpeedY = 200; | 0 |
scrollSpeed | 控制水平和垂直方向按键速度(单位毫秒) $tv.scrollSpeed = 200; | 0 |
方法
name | 描述 | 参数类型 |
---|---|---|
next(el|str) | 执行聚焦操作,可传期望的目标元素或方向标识 | Element 或 string |
requestFocus(el) | 执行聚焦操作,已在2.x被next取代 | Element |
getElementByPath(str) | 使用xPath获取element | string |
readXPath(el) | 根据DOM反向获取XPath路径 | Element |
resetScrollEl() | 重置成浏览器滚动 | – |
resetFocusClassName() | 重置 focusClassName | – |
resetInitDis() | 重置 initDis | – |
resetFindFocusType() | 重置 findFocusType | – |
resetKEYS() | 重置 KEYS | – |
resetOffsetDistance() | 重置 offsetDistance | – |
resetLongPressTime() | 重置 longPressTime | – |
resetDistanceToCenter() | 重置 distanceToCenter | – |
resetScrollEl() | 重置 scrollEl | – |
resetLimitingEl() | 重置 limitingEl | – |
resetFormAutofocus() | 重置 formAutofocus | – |
reset() | 重置所有配置 | – |
事件
name | 描述 |
---|---|
up | 上移 (可配置KEY_UP修改键值) |
right | 右移 (可配置KEY_RIGHT修改键值) |
down | 下移 (可配置KEY_DOWN修改键值) |
left | 左移 (可配置KEY_LEFT修改键值) |
onFocus | 获取焦点 |
onBlur | 失去焦点 |
longPress | 长按 (可配置KEY_ENTER修改键值), $tv.init.longPressTime可配置长按的时间,默认500ms |
click | 按enter时可触发click(可配置KEY_ENTER修改键值) |