目录
微信和鸿蒙Tabs效果对比
在安卓端微信客户端有这样一个效果,向左右滑动页面时底部的tabbar图标由灰色渐变到绿色,本篇博文将要通过ArkUI原生组件Tabs实现类似的效果
安卓微信页面滑动效果
在Tabs组件的使用过程中,如果使用CustomBuilder自定义了tabbar并通过onChange或onContentWillChange亦或onAnimationStart设置当前页面index,在点击tab实现切换可以通过属性动画或转场动画的形式实现tabbar的点击效果,但是如果开启了scrollable想要通过滑动页面的形式来切换TabContent会导致当前页到目标页之间的切换时tabbar的图标没有过渡效果,显得非常生硬。
Tabs组件默认效果(切换效果生硬)
Tabs实现滑动动效
本篇主要介绍通过Tabs组件的onGestureSwipe和onAnimationStart来实现页面间滑动时跟随页面滑动的动态效果,首先通过以下示例来简单介绍两个方法如何实现简单切换动效
简单示例
function quantize(value: number) {// 将偏移图标旋转角度进行线性转换
value = Math.abs(value)// 取绝对值
const clampedValue = Math.min(480, Math.max(0, value));// 假设480是页面的最大偏移量
return Math.round(clampedValue * 45 / 480);
}
@Entry
@Component
struct Index {
private tabsController: TabsController = new TabsController()
@State currentTarget:number = 480
@State currentTabIndex: number = 0
@State comeTabIndex:number = 0
iconRotate(index:number) : number{
// 设置激活Icon旋转角度
let res = 0
if(index === this.comeTabIndex){
res = quantize(this.currentTarget)
}else if(index === this.currentTabIndex){
res = 45 - quantize(this.currentTarget)
}
return res
}
@Builder
tabBar(index: number) {
Column() {
}
.width(36).height(36)
.backgroundColor(Color.Green)
.rotate({z:10,angle: this.iconRotate(index)})
.animation({duration:100,curve:Curve.Linear})
}
@Builder
content(text:string){Column(){
Text(text)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}.height("100%").width("100%").justifyContent(FlexAlign.End).padding({bottom:40})
}
build() {
Column() {
Tabs({ controller: this.tabsController }) {
TabContent() {
this.content("消息")
}.tabBar(this.tabBar(0))
TabContent() {
this.content("通讯录")
}.tabBar(this.tabBar(1))
TabContent() {
this.content("发现")
}.tabBar(this.tabBar(2))
TabContent() {
this.content("我")
}.tabBar(this.tabBar(3))
}
.onContentWillChange((currentIndex: number, comeIndex: number) => {
this.currentTabIndex = currentIndex
this.comeTabIndex = comeIndex
return true
})
.onGestureSwipe((index:number,event:TabsAnimationEvent)=>{
this.currentTarget = event.currentOffset
})
.onAnimationStart((index:number,targetIndex:number,event:TabsAnimationEvent)=>{
// 当页面滑动停止的时候会触发此回调,所以在这个回调中需要继续刷新currentTabIndex,以避免页面没有成功滑动到目标页的时候让index正常与页面对应
this.currentTabIndex = index
this.comeTabIndex = targetIndex
this.currentTarget = 480 //假设页面最大偏移量为480
})
.barPosition(BarPosition.End)
.edgeEffect(EdgeEffect.None)
.scrollable(true)
}
}
}
示例效果
以上示例是通过在onGestureSwipe回调中通过得到页面的偏移量,实时将滑动态的值通过线性转换为方块旋转的角度,示例中设定的最大偏移量假设为480,对应图标旋转的角度为45°,实际可通过屏幕宽度或组件Tabs组件组件宽度来设置,其中由两个比较关键的变量,分别是currentTabIndex和comeTabIndex,分别代表的是当前页的inde和目标页的索引,在iconRotate通过当前偏移量分别为所属索引的方块按不同方向旋转。
实现类似微信Tabs的图标颜色渐变动效
实现思路:通过获取页面间滑动时的偏移量线性转换为[0,1](透明度取值范围),在自定义tabbar中使用Stack将激活状态和未激活状态下的两个图标层叠在一起,滑动时将实时偏移量的对应透明度传递给图片组件,激活图标透明度由0到1,未激活图标的透明度由1到0,在视觉上便呈现出了渐显和渐隐的效果
实现代码
const MAX_OFFSET:number = 378 //假设页面最大偏移量为378
function linearMap(value:number, origMin:number, origMax:number, targetMin = 0, targetMax = 1) {
// value取绝对值
value = Math.abs(value)
// 处理原始区间无效的情况
if (origMin === origMax) {
return (targetMin + targetMax) / 2; // 返回目标区间中值
}
// 计算原始区间比例
const normalized = (value - origMin) / (origMax - origMin);
// 扩展到目标区间
const result = normalized * (targetMax - targetMin) + targetMin;
// 约束结果在目标区间(自动处理反向区间)
let bind:number[] = [Math.min(targetMin, targetMax), Math.max(targetMin, targetMax)];
return Math.min(bind[1], Math.max(bind[0], result));
}
@Entry
@Component
struct Index {
private tabsController: TabsController = new TabsController()
@State currentOffset:number = MAX_OFFSET
@State currentTabIndex: number = 0
@State comeTabIndex:number = 0
// 通过target动态改变透明度
imageOpacity(index: number, isSelectIcon: boolean): number {
const lmValue = linearMap(this.currentOffset, 0, MAX_OFFSET);
let res = isSelectIcon ? 0 : 1;
if (index === this.currentTabIndex) {
res = isSelectIcon ? 1 - lmValue : lmValue;
}
if (index === this.comeTabIndex) {
res = isSelectIcon ? lmValue : 1 - lmValue;
}
return res;
}
@Styles
imageStyle(){
.width("100%").height("100%")
.animation({duration:100,curve:Curve.Linear})
}
@Builder
customTabBar(index: number,selectIcon:ResourceStr,unSelectIcon:ResourceStr,label:string) {
Column() {
Stack(){
Image(selectIcon).imageStyle()
.opacity(this.imageOpacity(index,true))
Image(unSelectIcon).imageStyle()
.opacity(this.imageOpacity(index,false))
}.width(28).height(28)
Stack(){ // label同样可以使用图标的方式来实现颜色渐变
Text(label)
.fontSize(14).fontColor("#45c01a")
.animation({duration:100,curve:Curve.Linear})
.opacity(this.imageOpacity(index,true))
Text(label)
.fontSize(14).fontColor("#252525")
.opacity(this.imageOpacity(index,false))
.animation({duration:100,curve:Curve.Linear})
}.margin({top:6})
}
}
@Builder
content(text:string){Column(){
Text(text)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}.height("100%").width("100%").justifyContent(FlexAlign.End).padding({bottom:40})
}
build() {
Column() {
Tabs({ controller: this.tabsController }) {
TabContent() {
this.content("消息")
}.tabBar(this.customTabBar(0,$r("app.media.al_"),$r("app.media.ala"),"消息"))
TabContent() {
this.content("通讯录")
}.tabBar(this.customTabBar(1,$r("app.media.al8"),$r("app.media.al9"),"通讯录"))
TabContent() {
this.content("发现")
}.tabBar(this.customTabBar(2,$r("app.media.alb"),$r("app.media.alc"),"发现"))
TabContent() {
this.content("我")
}.tabBar(this.customTabBar(3,$r("app.media.ald"),$r("app.media.ale"),"我"))
}
.onContentWillChange((currentIndex: number, comeIndex: number) => {
this.currentTabIndex = currentIndex
this.comeTabIndex = comeIndex
return true
})
.onGestureSwipe((index:number,event:TabsAnimationEvent)=>{
this.currentOffset = event.currentOffset
})
.onAnimationStart((index:number,targetIndex:number,event:TabsAnimationEvent)=>{
// 当页面滑动停止的时候会触发此回调,所以在这个回调中需要继续刷新currentTabIndex,以避免页面没有成功滑动到目标页的时候让index正常与页面对应
this.currentTabIndex = index
this.comeTabIndex = targetIndex
this.currentOffset = MAX_OFFSET
})
.barPosition(BarPosition.End)
.edgeEffect(EdgeEffect.None) // 关闭页面首尾页的滑动效果
.scrollable(true)
}
}
}
最终效果
完整代码https://gitcode.com/mtyee/swipeTabsAnimation.git