# (Vue)MetroWidget

# 1.基本介绍

MetroWidget组件用于 JsView-Vue3 架构。 该组件提供自动排布和焦点管理功能

# 2.控件说明

# 属性设置(props)

  • name: [String]
    • 焦点ID, 具体参见JsvFocusBlock
  • top, left, width, height: [int]
    • 控件的位置尺寸信息
  • direction: [Symbol]
    • 控件方向, 值为 HORIZONTAL / VERTICAL
      • HORIZONTAL: 默认值, item排布向右延伸, 按从上到下, 从左到右的顺序排布
      • VERTICAL: item排布向下延伸, 按从左到右,从上到下的顺序排布
  • provideData: [() => Array]
    • 提供描画数据
  • layoutType: [String]
    • 布局的类型, 值为 relative/absolute
      • relative: 默认值, 根据item的尺寸自动排布
      • absolute: 根据item的绝对坐标进行布局
  • measures: [(itemData: Object) => Object]
    • 返回item描画相关信息
      • 返回对象说明
        • left, top: [int] layoutType为absolute时的坐标
        • width, height: [int] item的尺寸, 注意item的尺寸大于组件排布区域(组件尺寸减去padding)时会导致错误
        • marginRight, marginBottom: [int] item与右/底方向相邻item的间距
        • focusable: [boolean] item是否可以获得焦点
        • findNextAnchor: [Object] 自定义上下左右四条边寻找临近item的基准位置,值为0-1 {left: 0.5, right: 0.5, top: 0.5, bottom: 0.5}
        • itemSlide: [number] item的滚动控制, 可选值为:
          • METRO_WIDGET_CONST.ITEM_SLIDE.DISABLE 不滚动
          • METRO_WIDGET_CONST.ITEM_SLIDE.ACT_ITEM_FOCUS 按照SlideSetting滚动
          • METRO_WIDGET_CONST.ITEM_SLIDE.ACT_FOCUS_RECT_EVENT 按照子组件发送的focusRect事件滚动
        • zIndex: [int | object] item的zIndex, 为object时可以分别设置{focus: 0, normal: 0}, 为number时则同时设置获焦和不获焦的zIndex
        • uid: [string] item的uid, 可以通过uid来设置焦点
        • permanent: [boolean] 是否永久保留
        • enableTap: [boolean] 是否接受触控tap
        • showSkeleton: [boolean] 打断描画时, 是否显示骨架图
  • padding: [{left: int ,top: int, right: int, bottom: int}]
    • 描画item时的内边距, 组件的可排布区域为组件尺寸减去padding
  • focusMoveType: [int]
    • 焦点移动模式, 值为FocusMoveType中定义的类型
      • NO_ADJUST: 无特殊处理
      • COLUMN_LOOP: 到达列首/尾后跳转到上/下一列, 只在水平滚动时生效
      • ROW_LOOP: 到达行首/尾后跳转到上/下一行, 只在竖直滚动时生效
      • COLUMN_FIND_NEAR: 一列中没有下一个元素时是否跳转到相邻列
      • ROW_FIND_NEAR: 一行中没有下一个元素时是否跳转到相邻行
  • initFocusId: [int]
    • 组件首次创建时的初始焦点
  • slideSetting: [WholePageSlide | SeamlessSlide | FixPositionSlide]
    • 滚动设置, 只为三种设置类的对象, 具体见SlideSetting
      • WholePageSlide: 整页滚动设置
      • SeamlessSlide: 无缝滚动设置
      • FixPositionSlide: 固定位滚动设置
  • enableItemRenderBreak: [boolean]
    • 按键事件是否能打断item的描画, 默认为false
  • placeHolderSetting: [{backgroundColor: string, borderRadius: string, gap: int}]
    • item描画可打断时, 显示的占位符
      • backgroundColor: [String] 颜色
      • focusBackgroundColor: [String] 获焦时的颜色
      • borderRadius: [String] 圆角设置
      • gap: [int] 间隔(弃用)
  • sendFocusRectEvent: [boolean]
    • item获焦时在焦点树上冒泡一个事件, MetroWidget接收到事件时会触发滚动操作, 一般用于嵌套时的滚动
  • onFocus: [() => Void]
    • 获焦的回调, 具体见JsvFocusBlock
  • onBlur: [() => Void]
    • 失焦的回调, 具体见JsvFocusBlock
  • onEdge: [(info: {direction: Symbol, rect: Object}) => Void]
    • 焦点移动到边缘时的回调
      • direction: [Symbol] 边缘方向
      • rect: [{x: int, y: int, width: int, height: int}] 到达边缘时的区域
  • onScroll: [(visibleStart: int, visibleRange: int, totalSize: int) => Void]
    • 滚动时的回调
      • visibleStart: [int] 可视区域的起点
      • visibleRange: [int] 可视区域大小(不含padding)
      • totalSize: [int] 所有item的总长
  • loadAll: [boolean]
    • 强制加载所有view, 包括不显示的view
  • disableClip: [boolean]
    • 取消对显示范围的裁剪
  • keepTraceRange: [number]
    • 可视区域前后多少屏的item在移出可视范围后不释放(默认移出可视区域的item会被释放)

# slotProps

  • data
    • 此item对应的原始数据
  • onAction
    • 供item组件注册回调的对象
  • query
    • 提供MetroWidget的内部状态和一些方法
      • id: [int] item的id,
      • position: [(index : int) => Object ] 获取 item 相对 MetroWidget 左上角坐标
        • return: [{left: int, top: int, width: int, height: int}]
      • templatePosition: {(index : int) => Object} 获取 item 的布局坐标, 一般用于自定义滚动
        • return: [{left: int, top: int, width: int, height: int}]
      • getCurrentFocusId: {() => Object } 获取当前焦点信息,
        • return: [{id: int, index: int}]
          • id: [int] 在可获得焦点元素列表中的索
          • index: [int] 在所有元素列表中的索引
      • slideTo: [(targetPosition : int, doAnim : boolean) => Void] 滚动到指定位置,注意这个位置是 item 的布局坐标,另外当某些item需要接管滚动时,itemConfig中takeOverSlide要为true。
        • params
          • targetPosition: [int] 目标位置
          • doAnim: [boolean] 是否做动画
  • onItemEdge
    • 当item组件内部有按键逻辑时, item到达边缘后通知MetroWidget的回调

# 方法

  • slideTo: [(position: int, doAnim: boolean) => Void]
    • 滚动到指定位置
      • params
        • position: [int] 目标位置
        • doAnim: [boolean] 是否做动画
  • slideToItem: [(index: int, doAnim: boolean) => Void]
    • 滚动到指定item, 具体位置由slideType属性决定
      • params
        • index: [int] 目标item的index
        • doAnim: [boolean] 是否做动画
  • setEnterFocusId: [(rectInfo: Object) => Void]
    • 设置焦点移入时的区域,组件将会根据此区域寻找最近的item
      • params
        • rectInfo: [{direction: EdgeDirection, rect: {x: int, y: int, width: int, height: int}}] 移入区域
  • setFocusId: [(id: int, needSlide: boolean, doAnim: boolean, extraSetting: Object) => Void]
    • 设置焦点
      • params
        • id: [int] 目标id
        • needSlide: [boolean] 是否需要滚动
        • doAnim: [boolean] 是否做动画
        • extraSetting: [{lockChildSlideEvent: boolean}] 设置焦点时忽略子发过来的滚动事件
  • refreshData: [(forceUpdate: boolean) => Void]
    • 刷新数据 refresh时的对比逻辑:
      1. 对比array里的object是否是同一个对象
      2. 对比array里的jsvKey字段, 若一致则认为该数据未改变
      • params
        • forceUpdate: [boolean] 是否强制刷新
  • getVisibleItems: [() => {start: int, end: int, dataList: Array}]
    • 获取可视item的列表信息
      • return: [{start: int, end: int, dataList: Array}]
        • start: [int] 开始的index
        • end: [int] 结尾的index
        • dataList: [Arrya] 可视item对应的原始数据列表
  • lock: [(type: int) => () => Void)]
    • 设置内部的锁
      • params
        • type: [int] 锁的类型, 由METRO_WIDGET定义的参数通过|组合而成
          • SLIDE: 滚动动画锁
          • CHILD_SLIDE_EVENT: 子组件发送的滚动事件锁
      • return: [() => Void] 解锁的函数
  • unlock: [(type: int) => Void]
    • 解锁
      • params
        • type: [int] 同lock
  • setZIndex [(index: int, blurZIndex: int, focusZIndex: int) => Void]
    • 设置item的zIndex
      • params
        • index: [int] item的index
        • blurZIndex: [int] 未获焦时的zIndex
        • focusZIndex: [int] 获焦时的zIndex, 缺省时使用blurZIndex的值

# SlideSetting {#slide-setting}

  • WholePageSlide: 整页滚动设置
    • constructor: [new (settingObj: {speed: int, easing: string}) => WholePageSlide]
      • speed: 滚动速度, 像素/毫秒
      • easing: 动画的缓动函数
  • SeamlessSlide: 无缝滚动设置
    • constructor: [new (settingObj: {startPercent: float, endPercent: float, speed: int, easing: string}) => SeamlessSlide]
      • headRange: 头部触发滚动的区域, 取值范围0-1
      • tailRange: 尾部触发滚动的区域, 取值范围0-1
      • headSafeArea: 保证头部item完整显示的安全区域大小
      • tailSafeArea: 保证尾部item完整显示的安全区域大小
      • speed: 滚动速度, 像素/毫秒
      • easing: 动画的缓动函数
      • fixFirstPage: 首页不滚动
  • FixPositionSlide: 固定位滚动设置
    • constructor: [new (settingObj: {fixPercent: float, speed: int, easing: string}) => FixPositionSlide]
      • fixPercent: 固定的位置, 取值范围0-1
      • speed: 滚动速度, 像素/毫秒
      • easing: 动画的缓动函数
      • fixFirstPage: 首页不滚动

# 3.技巧展示

插槽如何使用?

  • 插槽属性参见slotProp

提问: item的onFocus/onBlur和JsvFocusBlock的onFocus/onBlur有何区别?

  • 整个MetroWidget在焦点树上只有一个节点, 既MetroWidget内部只有一个<jsv-focus-block/>, props中的onFocus/onBlur对应这个JsvFocusBlock
  • item的onFocus/onBlur对应的是MetroWidget自身内部状态的改变

按键是怎么处理的?

  • 上下左右键已经由控件接管,不需要开发者处理。
  • 通过注册onClick回调来处理用户的OK键动作。
  • 其他按键组件不会处理, 按JsvFocusBlock统一的冒泡处理

焦点到达边缘后怎么离开此组件?

  • 当焦点移动到控件边缘时,会调用onEdge回调。在回调中通过参数传递的值来决定焦点转移的行为

# 4.示例搭建

# demo.vue

  • 前置操作
    import {
      MetroWidget, 
      HORIZONTAL, // 定义MetroWidget方向的Symbol
      EdgeDirection, // MetroWidget onEdge回调中的方向比对
      useFocusHub,
    } from "jsview";
    import { onMounted } from "vue";
    import Item from "./Item.vue" // 描画item的组件
    
    const focusHub = useFocusHub();
    
    // mounted后设置焦点
    onMounted(() => { focusHub.setFocus(name); })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • template中添加MetroWidget组件
    <template>
      <metro-widget
        :name="name"
        :left="widgetLayout.left"
        :top="widgetLayout.top"
        :width="widgetLayout.width"
        :height="widgetLayout.height"
        :provideData="provideData"
        :direction="HORIZONTAL"
        :measures="measures"
        :onEdge="onEdge"
      >
      </metro-widget>
    </template>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • 在script中定义MetroWidget所需的prop
    //组件的焦点ID
    const name = "widget"
    //组件的布局信息
    const widgetLayout = {
      left: 50,
      top: 50,
      width: 300,
      height: 200,
    }
    //提供数据的回调
    const provideData = () => {
      const data = [];
      for (let i = 0; i < 30; i++) {
        data.push({
          width: 90,
          height: 90,
          marginRight: 10,
          marginBottom: 10,
          color: "#00DD00",
          content: i,
        });
      }
      return data;
    }
    // 提供item布局信息的回调
    const measures = (data) => {
      return {
        width: data.width,
        height: data.height,
        marginRight: data.marginRight,
        marginBottom: data.marginBottom,
      };
    };
    //组件到达边缘时的回调
    const onEdge = (edgeInfo) => {
      if (edgeInfo.direction == EdgeDirection.top) {
        console.log("top edge");
      } else {
        console.log("other side edge")
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
  • 添加MetroWidget的slot, 并在slot中描画item
    <template>
      <metro-widget
        ...
      >
        <template #renderItem="{ data, onAction }">
          <item :data="data" :onAction="onAction" />
        </template>
      </metro-widget>
    </template>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • 全部代码
    <script setup>
    import {
      MetroWidget,
      HORIZONTAL, // 定义MetroWidget方向的Symbol
      EdgeDirection, // MetroWidget onEdge回调中的方向比对
      useFocusHub,
    } from "jsview";
    import Item from "./Item.vue"
    import { onMounted } from "vue";
    
    const focusHub = useFocusHub();
    
    // mounted后设置焦点
    onMounted(() => {
      focusHub.setFocus(name);
    });
    
    //组件的焦点ID
    const name = "widget";
    //组件的布局信息
    const widgetLayout = {
      left: 50,
      top: 50,
      width: 300,
      height: 200,
    };
    //提供数据的回调
    const provideData = () => {
      const data = [];
      for (let i = 0; i < 30; i++) {
        data.push({
          width: 90,
          height: 90,
          marginRight: 10,
          marginBottom: 10,
          color: '#00DD00',
          content: i,
        });
      }
      return data;
    };
    // 提供item布局信息的回调
    const measures = (data) => {
      return {
        width: data.width,
        height: data.height,
        marginRight: data.marginRight,
        marginBottom: data.marginBottom,
      };
    };
    //组件到达边缘时的回调
    const onEdge = (edgeInfo) => {
      if (edgeInfo.direction == EdgeDirection.top) {
        console.log("top edge");
      } else {
        console.log("other side edge");
      }
    };
    </script>
    
    
      <template>
      <metro-widget
        :name="name"
        :left="widgetLayout.left"
        :top="widgetLayout.top"
        :width="widgetLayout.width"
        :height="widgetLayout.height"
        :provideData="provideData"
        :direction="HORIZONTAL"
        :measures="measures"
        :onEdge="onEdge"
      >
        <template #renderItem="{ data, onAction }">
          <item :data="data" :onAction="onAction" />
        </template>
      </metro-widget>
    </template>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78

# item.vue

  • 主要操作
    • 根据数据描画内容
    • 向onAction对象注册回调
    • 在回调中更改自身状态
  • 全部代码
    <script setup>
    import { ref, shallowRef, inject } from "vue";
    
    const props = defineProps({
      data: Object,
      onAction: Object,
    });
    
    const focused = ref(false);
    //在回调中更改自身状态
    const onFocus = () => {
      focused.value = true;
    };
    const onBlur = () => {
      focused.value = false;
    };
    const onClick = () => {
      console.log("item onclick ", props.data);
    };
    // 注册回调
    props.onAction.register("onFocus", onFocus);
    props.onAction.register("onBlur", onBlur);
    props.onAction.register("onClick", onClick);
    </script>
    
    <template>
      <div
        :style="{
          width: data.width,
          height: data.height,
          fontSize: 30,
          color: focused ? '#FF0000' : '#FFFFFF',
          backgroundColor: data.color,
        }"
      >
        {{ data.content }}
      </div>
    </template>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
Last Updated: 6/27/2024, 5:44:11 AM