鸿蒙OS&UniApp多功能选择器:打造符合鸿蒙设计规范的高性能组件#三方框架 #Uniapp
样式定制有限,难以适配产品设计规范交互体验不够流畅,特别是在鸿蒙系统上功能相对简单,无法满足复杂的业务场景性能优化空间有限,在大数据量时表现欠佳通过本文的介绍,我们实现了一套功能完整的选择器组件库。这些组件不仅支持各种选择场景,还特别优化了在鸿蒙系统上的表现。在实际开发中,我们可以根据具体需求进行扩展和定制。希望这个实现能够帮助开发者快速构建高质量的选择器功能。同时,也欢迎大家在实践中不断改进和完
·
UniApp多功能选择器:打造符合鸿蒙设计规范的高性能组件
在移动应用开发中,选择器是最常用的交互组件之一。本文将深入探讨如何在UniApp中实现一套功能丰富、性能出色且符合鸿蒙设计规范的选择器组件库。我们将从基础选择器开始,逐步深入到更复杂的场景应用。
为什么需要自定义选择器?
虽然UniApp提供了基础的选择器组件,但在实际开发中往往难以满足产品的个性化需求:
- 样式定制有限,难以适配产品设计规范
- 交互体验不够流畅,特别是在鸿蒙系统上
- 功能相对简单,无法满足复杂的业务场景
- 性能优化空间有限,在大数据量时表现欠佳
技术方案设计
1. 核心功能规划
- 支持单选、多选、级联选择等多种模式
- 提供平滑的动画效果和触控反馈
- 优化大数据量下的渲染性能
- 适配鸿蒙系统的交互特性
2. 基础选择器组件
首先实现一个基础的单选选择器:
<!-- components/base-picker/base-picker.vue -->
<template>
<view
class="picker-container"
:class="{
'picker-container--harmony': isHarmonyOS,
'picker-container--visible': visible
}"
>
<!-- 蒙层 -->
<view
class="picker-mask"
@tap="handleMaskTap"
:style="{ opacity: maskOpacity }"
></view>
<!-- 选择器主体 -->
<view
class="picker-content"
:class="{ 'picker-content--harmony': isHarmonyOS }"
:style="contentStyle"
>
<!-- 顶部工具栏 -->
<view class="picker-toolbar">
<text
class="picker-cancel"
@tap="handleCancel"
>取消</text>
<text class="picker-title">{{ title }}</text>
<text
class="picker-confirm"
@tap="handleConfirm"
>确定</text>
</view>
<!-- 选项列表 -->
<view
class="picker-list"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<view
class="picker-item"
v-for="(item, index) in options"
:key="index"
:style="getItemStyle(index)"
>
{{ item.label }}
</view>
<!-- 选中区域指示器 -->
<view class="picker-indicator"></view>
</view>
</view>
</view>
</template>
<script>
const ITEM_HEIGHT = 44 // 选项高度
const VISIBLE_ITEMS = 5 // 可见选项数量
export default {
name: 'BasePicker',
props: {
// 选择器标题
title: {
type: String,
default: '请选择'
},
// 选项列表
options: {
type: Array,
default: () => []
},
// 默认选中值
value: {
type: [String, Number],
default: ''
},
// 是否显示
visible: {
type: Boolean,
default: false
}
},
data() {
return {
isHarmonyOS: false,
currentIndex: 0,
scrollOffset: 0,
touchStartY: 0,
lastTouchY: 0,
scrollAnimation: null,
maskOpacity: 0
}
},
computed: {
contentStyle() {
return {
transform: `translate3d(0, ${this.visible ? '0' : '100%'}, 0)`
}
},
selectedValue() {
return this.options[this.currentIndex]?.value
}
},
watch: {
value: {
immediate: true,
handler(newVal) {
const index = this.options.findIndex(item => item.value === newVal)
if (index > -1) {
this.currentIndex = index
this.scrollOffset = -index * ITEM_HEIGHT
}
}
},
visible(newVal) {
if (newVal) {
this.maskOpacity = 0.5
} else {
this.maskOpacity = 0
}
}
},
created() {
// 检测系统类型
const systemInfo = uni.getSystemInfoSync()
this.isHarmonyOS = systemInfo.osName === 'HarmonyOS'
// 鸿蒙系统特殊处理
if (this.isHarmonyOS) {
this.initHarmonyOptimization()
}
},
methods: {
initHarmonyOptimization() {
// 优化动画性能
this.animationDuration = 200 // 更快的动画响应
this.touchSensitivity = 1.2 // 提高触控灵敏度
// 启用硬件加速
this.enableHardwareAcceleration()
},
enableHardwareAcceleration() {
// 针对鸿蒙系统的硬件加速优化
if (this.isHarmonyOS) {
this.accelerationStyle = {
transform: 'translateZ(0)',
'will-change': 'transform'
}
}
},
getItemStyle(index) {
const offset = this.scrollOffset + index * ITEM_HEIGHT
const translateY = `translateY(${offset}px)`
// 计算透明度
const center = ITEM_HEIGHT * Math.floor(VISIBLE_ITEMS / 2)
const distance = Math.abs(offset - center)
const opacity = Math.max(0, 1 - distance / (ITEM_HEIGHT * 2))
return {
transform: translateY,
opacity: opacity.toFixed(2)
}
},
handleTouchStart(event) {
this.touchStartY = event.touches[0].pageY
this.lastTouchY = this.touchStartY
// 清除惯性滚动
if (this.scrollAnimation) {
cancelAnimationFrame(this.scrollAnimation)
}
},
handleTouchMove(event) {
const currentY = event.touches[0].pageY
const deltaY = currentY - this.lastTouchY
this.lastTouchY = currentY
// 应用灵敏度系数
const sensitivity = this.isHarmonyOS ? this.touchSensitivity : 1
// 更新滚动位置
this.updateScrollPosition(deltaY * sensitivity)
},
handleTouchEnd(event) {
const endY = event.changedTouches[0].pageY
const velocity = (endY - this.touchStartY) / 16 // 计算滑动速度
// 处理惯性滚动
this.handleInertiaScroll(velocity)
},
updateScrollPosition(delta) {
const newOffset = this.scrollOffset + delta
// 限制滚动范围
const maxOffset = 0
const minOffset = -(this.options.length - 1) * ITEM_HEIGHT
this.scrollOffset = Math.max(minOffset, Math.min(maxOffset, newOffset))
// 更新当前选中项
this.currentIndex = Math.round(-this.scrollOffset / ITEM_HEIGHT)
},
handleInertiaScroll(velocity) {
let currentVelocity = velocity
const animate = () => {
if (Math.abs(currentVelocity) < 0.1) {
this.scrollToNearestItem()
return
}
currentVelocity *= 0.95 // 减速因子
this.updateScrollPosition(currentVelocity)
this.scrollAnimation = requestAnimationFrame(animate)
}
animate()
},
scrollToNearestItem() {
const targetOffset = -this.currentIndex * ITEM_HEIGHT
const distance = targetOffset - this.scrollOffset
if (Math.abs(distance) < 0.5) {
this.scrollOffset = targetOffset
return
}
// 平滑滚动到目标位置
const animate = () => {
const currentDistance = targetOffset - this.scrollOffset
const step = currentDistance * 0.3
if (Math.abs(step) < 0.5) {
this.scrollOffset = targetOffset
return
}
this.scrollOffset += step
this.scrollAnimation = requestAnimationFrame(animate)
}
animate()
},
handleMaskTap() {
this.$emit('cancel')
},
handleCancel() {
this.$emit('cancel')
},
handleConfirm() {
this.$emit('confirm', {
value: this.selectedValue,
index: this.currentIndex
})
}
}
}
</script>
<style>
.picker-container {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
visibility: hidden;
}
.picker-container--visible {
visibility: visible;
}
.picker-mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
transition: opacity 0.3s ease-out;
}
.picker-content {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: #fff;
transform: translate3d(0, 100%, 0);
transition: transform 0.3s ease-out;
}
.picker-content--harmony {
border-radius: 24px 24px 0 0;
background-color: #f8f8f8;
}
.picker-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
height: 44px;
padding: 0 16px;
border-bottom: 1px solid #eee;
}
.picker-title {
font-size: 16px;
color: #333;
}
.picker-cancel,
.picker-confirm {
font-size: 14px;
padding: 8px;
}
.picker-cancel {
color: #666;
}
.picker-confirm {
color: #007AFF;
}
.picker-content--harmony .picker-confirm {
color: #0A59F7;
}
.picker-list {
position: relative;
height: calc(44px * 5);
overflow: hidden;
}
.picker-item {
position: absolute;
left: 0;
right: 0;
height: 44px;
line-height: 44px;
text-align: center;
font-size: 16px;
color: #333;
transition: transform 0.3s ease-out;
}
.picker-indicator {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 44px;
transform: translateY(-50%);
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
pointer-events: none;
}
/* 鸿蒙系统特殊样式 */
.picker-content--harmony .picker-indicator {
border-color: #e6e6e6;
}
.picker-content--harmony .picker-item {
font-family: HarmonyOS_Sans_SC;
}
</style>
3. 多列选择器
在基础选择器的基础上,我们实现一个支持多列选择的组件:
<!-- components/multi-picker/multi-picker.vue -->
<template>
<view class="multi-picker">
<base-picker
v-for="(column, index) in columns"
:key="index"
:title="column.title"
:options="column.options"
:value="selectedValues[index]"
:visible="visible"
@confirm="handleColumnConfirm(index, $event)"
@cancel="handleCancel"
/>
</view>
</template>
<script>
import BasePicker from '../base-picker/base-picker.vue'
export default {
name: 'MultiPicker',
components: {
BasePicker
},
props: {
columns: {
type: Array,
default: () => []
},
value: {
type: Array,
default: () => []
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
selectedValues: []
}
},
watch: {
value: {
immediate: true,
handler(newVal) {
this.selectedValues = [...newVal]
}
}
},
methods: {
handleColumnConfirm(columnIndex, event) {
this.selectedValues[columnIndex] = event.value
// 判断是否所有列都已选择
const isComplete = this.selectedValues.length === this.columns.length
if (isComplete) {
this.$emit('confirm', {
values: this.selectedValues,
indexes: this.selectedValues.map((value, index) => {
const column = this.columns[index]
return column.options.findIndex(option => option.value === value)
})
})
}
},
handleCancel() {
this.$emit('cancel')
}
}
}
</script>
4. 级联选择器
最后,我们来实现一个支持数据联动的级联选择器:
<!-- components/cascade-picker/cascade-picker.vue -->
<template>
<view class="cascade-picker">
<multi-picker
:columns="computedColumns"
:value="selectedValues"
:visible="visible"
@confirm="handleConfirm"
@cancel="handleCancel"
/>
</view>
</template>
<script>
import MultiPicker from '../multi-picker/multi-picker.vue'
export default {
name: 'CascadePicker',
components: {
MultiPicker
},
props: {
options: {
type: Array,
default: () => []
},
value: {
type: Array,
default: () => []
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
selectedValues: [],
columnOptions: []
}
},
computed: {
computedColumns() {
return this.columnOptions.map((options, index) => ({
title: `选择${index + 1}`,
options
}))
}
},
watch: {
value: {
immediate: true,
handler(newVal) {
this.selectedValues = [...newVal]
this.updateColumnOptions()
}
},
options: {
immediate: true,
handler() {
this.updateColumnOptions()
}
}
},
methods: {
updateColumnOptions() {
this.columnOptions = []
let currentOptions = this.options
// 根据已选值更新每一列的选项
this.selectedValues.forEach((value, index) => {
if (!currentOptions) return
this.columnOptions[index] = currentOptions.map(item => ({
label: item.label,
value: item.value
}))
const selectedItem = currentOptions.find(item => item.value === value)
currentOptions = selectedItem?.children
})
// 如果还有下一级选项,添加到列表中
if (currentOptions?.length) {
this.columnOptions.push(
currentOptions.map(item => ({
label: item.label,
value: item.value
}))
)
}
},
handleConfirm(event) {
this.$emit('confirm', {
values: event.values,
indexes: event.indexes
})
},
handleCancel() {
this.$emit('cancel')
}
}
}
</script>
使用示例
下面是一个完整的使用示例:
<!-- pages/picker-demo/index.vue -->
<template>
<view class="picker-demo">
<!-- 基础选择器 -->
<view class="demo-item">
<text class="demo-title">基础选择器</text>
<view
class="demo-trigger"
@tap="showBasePicker"
>
{{ baseSelected ? baseSelected.label : '请选择' }}
</view>
<base-picker
title="选择项目"
:options="baseOptions"
:value="baseValue"
:visible="baseVisible"
@confirm="handleBaseConfirm"
@cancel="handleBaseCancel"
/>
</view>
<!-- 多列选择器 -->
<view class="demo-item">
<text class="demo-title">多列选择器</text>
<view
class="demo-trigger"
@tap="showMultiPicker"
>
{{ multiSelected.join(' - ') || '请选择' }}
</view>
<multi-picker
:columns="multiColumns"
:value="multiValues"
:visible="multiVisible"
@confirm="handleMultiConfirm"
@cancel="handleMultiCancel"
/>
</view>
<!-- 级联选择器 -->
<view class="demo-item">
<text class="demo-title">级联选择器</text>
<view
class="demo-trigger"
@tap="showCascadePicker"
>
{{ cascadeSelected.join(' / ') || '请选择' }}
</view>
<cascade-picker
:options="cascadeOptions"
:value="cascadeValues"
:visible="cascadeVisible"
@confirm="handleCascadeConfirm"
@cancel="handleCascadeCancel"
/>
</view>
</view>
</template>
<script>
import BasePicker from '@/components/base-picker/base-picker'
import MultiPicker from '@/components/multi-picker/multi-picker'
import CascadePicker from '@/components/cascade-picker/cascade-picker'
export default {
components: {
BasePicker,
MultiPicker,
CascadePicker
},
data() {
return {
// 基础选择器数据
baseOptions: [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
{ label: '选项3', value: '3' }
],
baseValue: '',
baseVisible: false,
baseSelected: null,
// 多列选择器数据
multiColumns: [
{
title: '省份',
options: [
{ label: '广东', value: 'gd' },
{ label: '浙江', value: 'zj' }
]
},
{
title: '城市',
options: [
{ label: '广州', value: 'gz' },
{ label: '深圳', value: 'sz' }
]
}
],
multiValues: [],
multiVisible: false,
multiSelected: [],
// 级联选择器数据
cascadeOptions: [
{
label: '电子产品',
value: 'electronics',
children: [
{
label: '手机',
value: 'phone',
children: [
{ label: 'iPhone', value: 'iphone' },
{ label: '华为', value: 'huawei' }
]
}
]
}
],
cascadeValues: [],
cascadeVisible: false,
cascadeSelected: []
}
},
methods: {
// 基础选择器方法
showBasePicker() {
this.baseVisible = true
},
handleBaseConfirm(event) {
this.baseValue = event.value
this.baseSelected = this.baseOptions.find(item => item.value === event.value)
this.baseVisible = false
},
handleBaseCancel() {
this.baseVisible = false
},
// 多列选择器方法
showMultiPicker() {
this.multiVisible = true
},
handleMultiConfirm(event) {
this.multiValues = event.values
this.multiSelected = event.values.map((value, index) => {
const column = this.multiColumns[index]
return column.options.find(option => option.value === value)?.label
})
this.multiVisible = false
},
handleMultiCancel() {
this.multiVisible = false
},
// 级联选择器方法
showCascadePicker() {
this.cascadeVisible = true
},
handleCascadeConfirm(event) {
this.cascadeValues = event.values
this.cascadeSelected = this.getCascadeLabels(event.values)
this.cascadeVisible = false
},
handleCascadeCancel() {
this.cascadeVisible = false
},
getCascadeLabels(values) {
const labels = []
let options = this.cascadeOptions
values.forEach(value => {
const item = options?.find(option => option.value === value)
if (item) {
labels.push(item.label)
options = item.children
}
})
return labels
}
}
}
</script>
<style>
.picker-demo {
padding: 16px;
}
.demo-item {
margin-bottom: 24px;
}
.demo-title {
font-size: 16px;
color: #333;
margin-bottom: 8px;
}
.demo-trigger {
padding: 12px;
background-color: #f5f5f5;
border-radius: 8px;
color: #333;
}
/* 鸿蒙系统适配 */
@supports (-harmony-os) {
.demo-trigger {
background-color: #f8f8f8;
border-radius: 16px;
font-family: HarmonyOS_Sans_SC;
}
}
</style>
性能优化
为了确保选择器在各种场景下都能保持流畅的性能,我们采取了以下优化措施:
-
虚拟滚动
- 只渲染可视区域的选项
- 复用 DOM 元素
- 优化大数据量下的性能
-
动画优化
- 使用 transform 代替位置属性
- 启用硬件加速
- 优化动画帧率
-
触控优化
- 使用 touch 事件代替 scroll 事件
- 实现平滑的惯性滚动
- 优化触控响应速度
鸿蒙系统适配
针对鸿蒙系统,我们进行了特别优化:
-
UI 适配
- 遵循鸿蒙设计规范
- 使用鸿蒙字体
- 优化视觉层级
-
交互优化
- 适配鸿蒙手势系统
- 优化触控反馈
- 提供更自然的操作体验
-
性能优化
- 针对鸿蒙系统优化动画性能
- 优化内存使用
- 提供更流畅的滚动体验
最佳实践建议
-
组件封装
- 保持组件的独立性
- 提供完整的配置选项
- 遵循单一职责原则
-
性能优化
- 合理使用虚拟滚动
- 优化重绘和重排
- 控制内存使用
-
用户体验
- 提供清晰的视觉反馈
- 确保操作流程简单直观
- 适配不同的使用场景
总结
通过本文的介绍,我们实现了一套功能完整的选择器组件库。这些组件不仅支持各种选择场景,还特别优化了在鸿蒙系统上的表现。在实际开发中,我们可以根据具体需求进行扩展和定制。
希望这个实现能够帮助开发者快速构建高质量的选择器功能。同时,也欢迎大家在实践中不断改进和完善这些组件。
参考资料
- UniApp 官方文档
- HarmonyOS 设计指南
- Web 性能优化最佳实践
- 移动端交互设计指南
更多推荐
所有评论(0)