目前开发安卓TV的方法相对开说是十分的少的,使用uniapp开发相对来说几乎是没有的,因此,写下这篇文章供大家参考。
开发难点
-
如何方便的开发调试
-
如何使需要被聚焦的元素获取聚焦状态
-
如何是被聚焦的元素滚动到视图中心位置
-
如何缓存切换路由的时,上一个页面的聚焦状态,再回来是还是聚焦状态
-
如何启用wgt和apk两种方式的升级
一、如何方便的开发调试
之前我在论坛看到人家说,没办法呀,电脑搬到电视,然后调试。
其实大可不必,安装android studio里边创建一个模拟器就可以了。
注意:最好安装和电视系统相同的版本号,我这里是长虹电视,安卓9所以使使用安卓9的sdk
二、如何使需要被聚焦的元素获取聚焦状态
uniapp的本质上是webview, 因此我们可以在它的元素上添加tabIndex, 就可以获取焦点了。
xml
<view class="card" tabindex="0"> <image :src="`${VITE_URL}${props.image}`" fade-show lazy-load mode="aspectFill"></image> <view class="bottom"> <text class="name">{{ props.name }}</text> <text class="remark">{{ props.remark }}</text> <div class="footer"> <view class="tags"> <text class="tag" v-for="tag in tags" :key="tag">{{ tag }}</text> </view> <text class="price">¥ {{ props.price }}</text> </div> </view> </view>
.card { border-radius: 1.25vw; overflow: hidden;}.card:focus { box-shadow: 0 0 0 0.3vw #fff, 0 0 1vw 0.3vw #333; outline: none; transform: scale(1.03); transition: box-shadow 0.3s ease, transform 0.3s ease;}
三、如何是被聚焦的元素滚动到视图中心位置
使用renderjs进行实现如下
ruby
<script module="homePage" lang="renderjs">export default { mounted(){ let isScrolling = false; // 添加一个标志位,表示是否正在滚动 document.body.addEventListener("focusin", (e) => { if (!isScrolling) { // 检查是否正在滚动 isScrolling = true; // 设置滚动标志为true requestAnimationFrame(() => { // @ts-ignore e.target.scrollIntoView({ behavior: 'smooth', // @ts-ignore block:e.target.dataset.index? "end":'center' }); isScrolling = false; // 在滚动完成后设置滚动标志为false }); } }); }}</script>
就可以使被聚焦元素滚动到视图中心,requestAnimationFrame的作用是缓存
四、如何缓存切换路由的时,上一个页面的聚焦状态,再回来是还是聚焦状态
通过设置tabindex属性为0和1,会有不同的效果:
-
tabindex="0":将元素设为可聚焦,并按照其在文档中的位置来确定焦点顺序。当使用Tab键进行键盘导航时,tabindex="0"的元素会按照它们在源代码中的顺序获取焦点。这可以用于将某些非交互性元素(如
、等)设为可聚焦元素,使其能够被键盘导航。
-
tabindex="1":将元素设为可聚焦,并将其置于默认的焦点顺序之前。当使用Tab键进行键盘导航时,tabindex="1"的元素会在默认的焦点顺序之前获取焦点。这通常用于重置焦点顺序,或者将某些特定的元素(如重要的输入字段或操作按钮)置于首位。
需要注意的是,如果给多个元素都设置了tabindex属性,那么它们的焦点顺序将取决于它们的tabindex值,数值越小的元素将优先获取焦点。如果多个元素具有相同的tabindex值,则它们将按照它们在文档中的位置来确定焦点顺序。同时,负数的tabindex值也是有效的,它们将优先于零和正数值获取焦点。
我们要安装缓存插件,如pinia或vuex,需要缓存的页面单独配置
javascript
import { defineStore } from "pinia";export const useGlobalStore = defineStore("global", { state: () => ({ home_active_tag: "active0", hot_active_tag: "hot0", dish_active_tag: "dish0", }),});
更新一下业务代码
typescript
组件区域
<template> <view class="card" :tabindex="home_active_tag === 'packagecard' + props.id ? 1 : 0"> <image :src="`${VITE_URL}${props.image}`" fade-show lazy-load mode="aspectFill"></image> <view class="bottom"> <text class="name">{{ props.name }}</text> <text class="remark">{{ props.remark }}</text> <div class="footer"> <view class="tags"> <text class="tag" v-for="tag in tags" :key="tag">{{ tag }}</text> </view> <text class="price">¥ {{ props.price }}</text> </div> </view> </view></template>
const { home_active_tag } = storeToRefs(useGlobalStore());
页面区域
<view class="content"> <FoodCard v-for="_package in list.dishes" @click="goShopByFood(_package)" :id="_package.id" :name="_package.name" :image="_package.image" :tags="_package.tags" :price="_package.price" :shop_name="_package.shop_name" :shop_id="_package.shop_id" :key="_package.id" ></FoodCard> <image class="card" @click="goMore" :tabindex="home_active_tag === 'more' ? 1 : 0" style="width: 29.375vw; height: 25.9375vw" src="/static/home/more.png" mode="aspectFill" /> </view>
const goShopByFood = async (row: Record<string, any>) => { useGlobalStore().home_active_tag = "foodcard" + row.id; uni.navigateTo({ url: `/pages/shop/index?shop_id=${row.shop_id}`, animationDuration: 500, animationType: "zoom-fade-out", });};
如果,要设置启动默认焦点 id和index可默认设置,推荐启动第一个焦点组用index,它可以确定
arduino
<view class="active"> <image v-for="(active, i) in list.active" :key="active.id" @click="goActive(active, i)" :tabindex="home_active_tag === 'active' + i ? 1 : 0" :src="`${VITE_URL}${active.image}`" data-index="0" fade-show lazy-load mode="aspectFill" class="card" ></image> </view>
import { defineStore } from "pinia";export const useGlobalStore = defineStore("global", { state: () => ({ home_active_tag: "active0", 默认选择 hot_active_tag: "hot0", dish_active_tag: "dish0", }),});
对于多层级的,要注意销毁,在前往之前设置默认焦点
typescript
const goHot = (index: number) => { useGlobalStore().home_active_tag = "hotcard" + index; useGlobalStore().hot_active_tag = "hot0"; uni.navigateTo({ url: `/pages/hot/index?index=${index}`, animationDuration: 500, animationType: "zoom-fade-out", });};
五、如何启用wgt和apk两种方式的升级
pages.json
json
{ "path": "components/update/index", "style": { "disableScroll": true, "backgroundColor": "#0068d0", "app-plus": { "backgroundColorTop": "transparent", "background": "transparent", "titleNView": false, "scrollIndicator": false, "popGesture": "none", "animationType": "fade-in", "animationDuration": 200 } } }
组件
xml
<template> <view class="update"> <view class="content"> <view class="content-top"> <text class="content-top-text">发现版本</text> <image class="content-top" style="top: 0" width="100%" height="100%" src="@/static/bg_top.png"> </image> </view> <text class="message"> {{ message }} </text> <view class="progress-box"> <progress class="progress" border-radius="35" :percent="progress.progress" activeColor="#3DA7FF" show-info stroke-width="10" /> <view class="progress-text"> <text>安装包正在下载,请稍后,系统会自动重启</text> <text>{{ progress.totalBytesWritten }}MB/{{ progress.totalBytesExpectedToWrite }}MB</text> </view> </view> </view> </view></template><script setup lang="ts">import { onLoad } from "@dcloudio/uni-app";import { reactive, ref } from "vue";const message = ref("");const progress = reactive({ progress: 0, totalBytesExpectedToWrite: "0", totalBytesWritten: "0" });onLoad((query: any) => { message.value = query.content; const downloadTask = uni.downloadFile({ url: `${import.meta.env.VITE_URL}/${query.url}`, success(downloadResult) { plus.runtime.install( downloadResult.tempFilePath, { force: false, }, () => { plus.runtime.restart(); }, (e) => {} ); }, }); downloadTask.onProgressUpdate((res) => { progress.progress = res.progress; progress.totalBytesExpectedToWrite = (res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2); progress.totalBytesWritten = (res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2); });});</script><style lang="less">page { background: transparent; .update { /* #ifndef APP-NVUE */ display: flex; /* #endif */ justify-content: center; align-items: center; position: fixed; left: 0; top: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.65); .content { position: relative; top: 0; width: 50vw; height: 50vh; background-color: #fff; box-sizing: border-box; padding: 0 50rpx; font-family: Source Han Sans CN; border-radius: 2vw; .content-top { position: absolute; top: -5vw; left: 0; image { width: 50vw; height: 30vh; } .content-top-text { width: 50vw; top: 6.6vw; left: 3vw; font-size: 3.8vw; font-weight: bold; color: #f8f8fa; position: absolute; z-index: 1; } } } .message { position: absolute; top: 15vw; font-size: 2.5vw; } .progress-box { position: absolute; width: 45vw; top: 20vw; .progress { width: 90%; border-radius: 35px; } .progress-text { margin-top: 1vw; font-size: 1.5vw; } } }}</style>
App.vue
vbnet
<script setup lang="ts">import { onLaunch } from "@dcloudio/uni-app";import { useRequest } from "./hooks/useRequest";import dayjs from "dayjs";onLaunch(() => { // #ifdef APP-PLUS plus.runtime.getProperty("", async (app) => { const res: any = await useRequest("GET", "/api/tv/app"); if (res.code === 2000 && res.row.version > (app.version as string)) { uni.navigateTo({ url: `/components/update/index?url=${res.row.url}&type=${res.row.type}&content=${res.row.content}`, fail: (err) => { console.error("更新弹框跳转失败", err); }, }); } }); // #endif});</script>
如果要获取启动参数
arduino
plus.android.importClass("android.content.Intent"); const MainActivity = plus.android.runtimeMainActivity(); const Intent = MainActivity.getIntent(); const roomCode = Intent.getStringExtra("roomCode"); if (roomCode) { uni.setStorageSync("roomCode", roomCode); } else if (!uni.getStorageSync("roomCode") && !roomCode) { uni.setStorageSync("roomCode", "8888"); }