本篇内容:动画的学习使用
一、 知识储备
1. 布局更新动画
- 包含显式动画(animateTo)和属性动画(animation)
动画类型名称 | 特点 |
---|
显式动画 | 闭包内的变化都会触发动画执行, 可以做较复杂的动画 |
属性动画 | 属性变化时触发动画执行, 设置简单 |
说白了,显示动画就是靠闭包事件触发,属性动画是挂在组件身上的属性变化触发
- 显式动画 可以通过修改组件的布局方式、宽高、位置触发动画
animateTo({ duration: 1000, curve: Curve.Ease }, () => {
// 动画闭包中根据标志位改变控制第一个Button宽高的状态变量,使第一个Button做宽高动画
if (this.flag) {
this.myWidth = 100;
this.myHeight = 50;
} else {
this.myWidth = 200;
this.myHeight = 100;
}
this.flag = !this.flag;
});
Image($r('app.media.ic_hm_logo'))
.width(160)
.height(160)
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
right: { anchor: '__container__', align: this.itemAlign }
})
.margin({ top: this.mTop, right: 35 })
.id('logo')
.animation({ duration: 1000, curve: Curve.Ease }) //animation只对上面的设置生效
.onClick(() => {
this.mTop = 111
})
2. 组件内转场动画(transition)
Button() //删除插入同一个动画
.transition({ type: TransitionType.All, scale: { x: 0, y: 0 } })
Button()//删除插入不同动画
.transition({ type: TransitionType.Insert, translate: { x: 200, y: -200 }, opacity: 0 })
.transition({ type: TransitionType.Delete, rotate: { x: 0, y: 0, z: 1, angle: 360 } })
3. 弹簧曲线动画
- 分为两类,一个springCurve,一个springMotion和responsiveSpringMotion。
- springCurve
springCurve(velocity: number, mass: number, stiffness: number, damping: number)
//velocity: 初速度
//mass:弹簧系统的质量
//stiffness:弹簧系统的刚度
//damping:弹簧系统的阻尼
- springMotion和responsiveSpringMotion。此动画适合跟手动画,duration设置无效。跟手过程推荐使用responsiveSpringMotion,松手时使用springMotion
springMotion(response?: number, dampingFraction?: number, overlapDuration?: number)
responsiveSpringMotion(response?: number, dampingFraction?: number, overlapDuration?: number)
//response:可选参数 弹簧自然振动周期
//dampingFraction:可选参数阻尼系数
//overlapDuration:可选参数弹性动画衔接时长
- 注意: springCurve可以设置初速度,单一属性存在多个动画时,可以正常叠加动画效果。springMotion,由于开发者不可修改速度机制,所以在单一属性存在多个动画时,后一动画会取代前一动画。并继承前一动画的速度
.onTouch(event => {
if (event.type == TouchType.Move) {
animateTo({ curve: curves.responsiveSpringMotion() }, () => {
this.mRight = event.touches[0].screenX - 80;
})
} else if (event.type == TouchType.Up) {
animateTo({curve: curves.springMotion()}, ()=>{
this.mRight = 35;
})
}
})
4. 放大缩小动画
- sharedTransition,
- Exchange类型的共享元素转场动画,就是前后两个页面都设置有相同id的动画,那么转场时,将会自动触发缩放动画
.sharedTransition('sharedTransition', { duration: 1000, curve: Curve.Linear })
- Static类型的共享元素转场动画, 只需要在一个页面设置即可,在转换页面时自动触发透明度从0到该组件原设定的的透明度的动画。位置等其他属性不变
.sharedTransition('input', { duration: 500, curve: Curve.Linear, type: SharedTransitionEffectType.Static })
5. 页面转场动画
- 页面转场切换时,效果触发在页面上,在pageTransition函数中设置PageTransitionEntert和PageTransitionExit动画
PageTransitionEnter({type?: RouteType,duration?: number,curve?: Curve | string,delay?: number})
PageTransitionExit({type?: RouteType,duration?: number,curve?: Curve | string,delay?: number})
//type:默认是RouteType.None表示对页面栈的push和pop操作均生效,
//duration: 动画时长
//curve: 动画效果
//delay: 动画延时
转场动画设置
pageTransition() {
PageTransitionEnter({ type: RouteType.Push, duration: 1000, })
.slide(SlideEffect.Right)
PageTransitionEnter({ type: RouteType.Pop, duration: 1000, })
.slide(SlideEffect.Left)
PageTransitionExit({type: RouteType.Push, duration: 1000})
.slide(SlideEffect.Left)
PageTransitionExit({type: RouteType.Pop, duration: 1000})
.slide(SlideEffect.Right)
}
二、 效果一览
三、 源码剖析
import promptAction from '@ohos.promptAction';
import picker from '@ohos.file.picker';
import thermal from '@ohos.thermal';
import router from '@ohos.router';
import curves from '@ohos.curves';
@Component
@Entry
struct LoginPage {
@State account: string = '';
@State password: string = '';
dialog: CustomDialogController = new CustomDialogController({
builder: TipDialog({
cancel: this.onCancel,
commit: this.onCommit,
msg: this.account
}),
alignment: DialogAlignment.Center
});
onCancel() {
promptAction.showToast({ message: '取消登录' })
}
onCommit() {
promptAction.showToast({ message: '登录成功' })
router.pushUrl({ url: 'pages/router/MyRouter' })
}
@State color: Color = 0x1f2937;
@State itemAlign: HorizontalAlign = HorizontalAlign.Center
allAlign: HorizontalAlign[] = [HorizontalAlign.Center, HorizontalAlign.End]
alignIndex: number = 0;
@State mTop: number = 106
@State flag: boolean = true
@State translateX: number = 0;
@State mRight: number = 35;
build() {
Column() {
RelativeContainer() {
// Photo()
Text('欢迎登录')
.fontSize(25)
.fontColor(this.color)
.margin({ top: 111, left: 33 })
.id('tips')
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})
.width('100%')
.fontWeight(FontWeight.Bold)
Image($r('app.media.ic_hm_logo'))
.width(160)
.height(160)
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
right: { anchor: '__container__', align: this.itemAlign }
})
.margin({ top: this.mTop, right: this.mRight })
.id('logo')
.animation({ duration: 1000, curve: Curve.Ease }) //animation只对上面的设置生效 属性动画
.onTouch(event => {
if (event.type == TouchType.Move) {
animateTo({ curve: curves.responsiveSpringMotion() }, () => {
this.mRight = event.touches[0].screenX - 80;
})
} else if (event.type == TouchType.Up) {
animateTo({ curve: curves.springMotion() }, () => {
this.mRight = 35;
})
}
})
.onClick(() => {
if (this.mTop == 111) {
this.mTop = 106
} else {
this.mTop = 111;
}
animateTo({ curve: Curve.LinearOutSlowIn, duration: 1000 }, () => {
this.flag = !this.flag;
})
this.translateX = -1;
animateTo({ curve: curves.springCurve(2000, 1, 1, 1.2), duration: 1000 }, () => {
this.translateX = 0;
})
})
if (this.flag) {
Image($r('app.media.ic_hm_logo'))
.width(160)
.height(160)
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
right: { anchor: '__container__', align: this.itemAlign }
})
.margin({ top: 580, right: 35 })
.id('bottom_logo')
.translate({ x: this.translateX })
.transition({ type: TransitionType.Insert, opacity: 1, scale: { x: 0, y: 0 } })
.transition({ type: TransitionType.Delete, opacity: 0, scale: { x: 0, y: 0 } })
}
Image($r('app.media.ic_hm_logo'))
.width(160)
.height(160)
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
right: { anchor: '__container__', align: this.itemAlign }
})
.margin({ top: 600, right: 35 })
.id('bottom_animateTo')
.translate({ x: this.translateX })
.sharedTransition('sharedTransition', { duration: 1000, curve: Curve.Linear })
.onClick(() => {
router.pushUrl({ url: 'pages/router/MyRouter' })
})
Text('用户名:')
.fontSize(14)
.fontColor(this.color)
.alignRules({
top: { anchor: 'tips', align: VerticalAlign.Bottom },
left: { anchor: 'tips', align: HorizontalAlign.Start }
})
.margin({ top: 140, left: 33 })
.height(20)
.textAlign(TextAlign.Center)
.id('account')
.width(60)
TextInput({ placeholder: '请输入账号' })
.alignRules({
top: { anchor: 'account', align: VerticalAlign.Bottom },
left: { anchor: 'account', align: HorizontalAlign.Start }
})
.margin({ top: 13, left: 33, right: 33 })
.id("etAccount")
.height(46)
.fontColor(this.color)
.onChange(val => {
this.account = val;
})
Text('密码:')
.fontSize(14)
.fontColor('#333333')
.alignRules({
top: { anchor: 'etAccount', align: VerticalAlign.Bottom },
left: { anchor: 'etAccount', align: HorizontalAlign.Start }
})
.width(60)
.height(20)
.margin({ top: 15, left: 33 })
.textAlign(TextAlign.Center)
.id('password')
TextInput({ placeholder: '请输入密码' })
.alignRules({
top: { anchor: 'password', align: VerticalAlign.Bottom },
left: { anchor: 'password', align: HorizontalAlign.Start }
})
.type(InputType.Password)
.margin({ top: 13, left: 33, right: 33 })
.id("etPassword")
.height(46)
.onChange(val => {
this.password = val;
})
Toggle({ type: ToggleType.Checkbox })
.onChange((isChecked) => {
if (isChecked) {
promptAction.showToast({ message: "记住账号密码" })
} else {
promptAction.showToast({ message: "不记住密码" })
}
})
.alignRules({
top: { anchor: 'password', align: VerticalAlign.Bottom },
left: { anchor: 'password', align: HorizontalAlign.Start }
})
.height(15)
.margin({
top: 80, left: 33
})
.id('isRecord')
Text('记住密码')
.fontColor("#999999")
.fontSize(12)
.height(15)
.margin({
top: 80,
left: 6
})
.alignRules({
top: { anchor: 'password', align: VerticalAlign.Bottom },
left: { anchor: 'isRecord', align: HorizontalAlign.End }
})
.id('tvRecord')
Button('登录', { type: ButtonType.Capsule, stateEffect: true })
.onClick(() => {
this.dialog.open()
animateTo({ duration: 10000, curve: Curve.LinearOutSlowIn }, () => { //显式动画
this.alignIndex = (this.alignIndex + 1) % this.allAlign.length;
this.itemAlign = this.allAlign[this.alignIndex]
})
})
.alignRules({
top: {
anchor: 'tvRecord', align: VerticalAlign.Bottom
},
left: { anchor: 'etAccount', align: HorizontalAlign.Start },
right: { anchor: 'etAccount', align: HorizontalAlign.End }
})
.height(47)
.margin({
right: 23,
top: 19,
left: 23
})
.backgroundColor(0x00c250)
.type(ButtonType.Capsule)
.id('btCommit')
}.width('100%')
.height('100%')
}
}
pageTransition() {
PageTransitionEnter({ type: RouteType.Push, duration: 1000, })
.slide(SlideEffect.Right)
PageTransitionEnter({ type: RouteType.Pop, duration: 1000, })
.slide(SlideEffect.Left)
PageTransitionExit({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Left)
PageTransitionExit({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Right)
}
}
@CustomDialog
struct TipDialog {
dialogController: CustomDialogController;
cancel: () => void
commit: () => void
msg: string;
build() {
Column() {
Text('确定登录当前账号“' + this.msg + '”吗')
.fontColor(Color.Red)
.fontSize(44)
Row() {
Button('确定')
.type(ButtonType.Capsule)
.onClick(() => {
this.commit()
this.dialogController.close()
})
Button('取消')
.type(ButtonType.Capsule)
.onClick(() => {
this.cancel()
this.dialogController.close()
})
}
}
}
}
@Component
struct Photo {
@State imgListData: string[] = [];
getAllImg() { //获取照片url集
try {
let photoSelectOptions = new picker.PhotoSelectOptions(); //媒体库选择器设置
photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; //图片类型
photoSelectOptions.maxSelectNumber = 5; //最多 选取五张
let photoPicker = new picker.PhotoViewPicker; //初始化图片选择器
photoPicker.select(photoSelectOptions).then(photoSelectResult => { //选择结果回调
this.imgListData = photoSelectResult.photoUris;
console.error(`imgListData size is ${this.imgListData.length}`)
}).catch(err => {
console.error(`select failed code is ${err.code}, msg : ${err.message}`)
})
} catch (err) {
console.error(`load photo failed ${err.code}, msg: ${err.message}`)
}
}
async aboutToAppear() {
this.getAllImg(); //获取图片url
}
build() {
}
}
import http from '@ohos.net.http';
import ResponseCode from '@ohos.net.http';
import image from '@ohos.multimedia.image';
import picker from '@ohos.file.picker';
import router from '@ohos.router';
@Entry
@Component
struct MyRouter {
private tabsController: TabsController = new TabsController();
@State currentIndex: number = 0;
@Builder TabBuild(title: string, targetIndex: number, SelectedImg: Resource, normalImg: Resource) {
Column() { //自定义tab
Image(this.currentIndex == targetIndex ? SelectedImg : normalImg)
.size({ width: 25, height: 25 })
Text(title)
.fontSize(16).fontColor(this.currentIndex == targetIndex ? 0x00c250 : 0x333333)
}
.width('100%')
.height(48)
.justifyContent(FlexAlign.Center)
.onClick(() => {
console.error(`targetIndex is ${targetIndex}`)
this.currentIndex = targetIndex;
this.tabsController.changeIndex(this.currentIndex)
})
}
@State image: PixelMap = undefined; //创建PixelMap状态变量
private imgUrl: string = 'https://img1.baidu.com/it/u=3241660985,1063915045&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1194';
loadImg() { //获取网络美女图片
http.createHttp().request(this.imgUrl, (err, data) => {
if (err) {
console.error(`err is ${JSON.stringify(err)}`)
} else {
let code = data.responseCode;
if (ResponseCode.ResponseCode.OK == code) {
let res: any = data.result;
let imageSource = image.createImageSource(res)
let options = {
alphaTye: 0, //透明度
editable: false, //是否可编辑
pixelFormat: 3, //像素格式
scaleMode: 1, //缩略值
size: { height: 100, wight: 100 } //创建图片大小
}
imageSource.createPixelMap(options).then(pixelMap => {
this.image = pixelMap;
})
}
}
})
}
aboutToAppear() {
// this.context = getContext();
this.loadImg()
}
settings: RenderingContextSettings = new RenderingContextSettings(true)
context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
offContext: OffscreenCanvasRenderingContext2D = new OffscreenCanvasRenderingContext2D(600, 600, this.settings)
img: ImageBitmap = new ImageBitmap('../../../resources/base/media/ic_hm_logo.svg')
scroller: Scroller = new Scroller();
@State _50: number = vp2px(50)
@State _100: number = vp2px(100)
@State _85: number = vp2px(85)
@State _15: number = vp2px(15)
@State _: number = vp2px(50)
build() {
Tabs({
barPosition: BarPosition.End,
controller: this.tabsController
}) { //end start 首尾位置设置 controller 绑定tabs的控制器
TabContent() { //内容页面组件
MyNavigation().backgroundColor(0xf7f7f7)
}
.tabBar(this.TabBuild('首页', 0, $r('app.media.ic_hm_home_selected'), $r('app.media.ic_hm_home_normal')))
TabContent() {
Image(this.image).height('100%').width('100%')
}
.tabBar(this.TabBuild('直播', 1, $r('app.media.ic_hm_living_selected'), $r('app.media.ic_hm_living_normal')))
TabContent() {
Scroll(this.scroller) {
Column() {
Row() {
Image(this.imgUrl)
.width('30%')
.height('20%')
.border({ width: 1 })
.objectFit(ImageFit.Contain) //等比缩放,图片完全显示
.margin(15)
.overlay('Contain', { align: Alignment.Bottom, offset: { x: 0, y: 30 } })
.colorFilter([ //添加滤镜效果
1, 1, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0
])
.syncLoad(true) //同步加载图片
.onComplete(msg => {
if (msg) {
console.error(`widthVal = ${msg.width}\n height = ${msg.height} \n componentW = ${msg.componentWidth} \n status = ${msg.loadingStatus}`)
}
})
.onError(() => {
console.error('err')
})
Image(this.imgUrl)
.width('30%')
.height('20%')
.border({ width: 1 })
.objectFit(ImageFit.Cover) //等比缩放, 图片显示部分
.margin(15)
.overlay('Cover', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image(this.imgUrl)
.width('30%')
.height('20%')
.border({ width: 1 })
.objectFit(ImageFit.Auto) //自适应显示
.margin(15)
}
Row() {
Image(this.imgUrl)
.width('30%')
.height('20%')
.border({ width: 1 })
.objectFit(ImageFit.Fill) //填充显示,不保持等比缩放
.renderMode(ImageRenderMode.Template) //图片渲染模式
.margin(15)
.overlay('fill', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image(this.imgUrl)
.sourceSize({ //设置图片解码尺寸
width: 65, height: 40
})
.width('30%')
.height('20%')
.border({ width: 1 })
.objectFit(ImageFit.ScaleDown) //保持原宽高比 等比缩放或不变
.objectRepeat(ImageRepeat.XY) //设置图片在xy轴上重复显示
.margin(15)
.overlay('scaleDown', { align: Alignment.Bottom, offset: { x: 0, y: 30 } })
Image(this.imgUrl)
.width('30%')
.height('20%')
.border({ width: 1 })
.objectFit(ImageFit.None) //保持图片原尺寸
.interpolation(ImageInterpolation.High) //图片插值,抗锯齿,使图片看着更清晰
.margin(15)
.overlay("none", { align: Alignment.Bottom, offset: { x: 0, y: 30 } })
}
}
}.width('100%')
.height('100%')
}
.tabBar(this.TabBuild('朋友圈', 2, $r('app.media.ic_hm_friend_selected'), $r("app.media.ic_hm_friend_normal")))
TabContent() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor(Color.Yellow)
.onReady(() => {
this.offContext.drawImage(this.image, 110, 400, 130, 130)
let imageData = this.offContext.getImageData(150, 450, 130, 130)
this.offContext.putImageData(imageData, 130, 130)
let img = this.offContext.transferToImageBitmap();
this.context.transferFromImageBitmap(img) //.截图画图
this.context.beginPath()
this.context.rect(110, 10, 210, 110)
this.context.stroke()
this.context.font = '46px'
this.context.fillText('矩形', 120, 20) //画矩形
this.context.beginPath()
this.context.arc(110, 150, 50, 0, 6.28)
this.context.stroke()
this.context.font = '56px'
this.context.fillText('圆形', 120, 220) //画圆形
this.context.beginPath()
this.context.ellipse(110, 250, 50, 100, Math.PI * 0.25, Math.PI * 0, Math.PI * 2)
this.context.stroke() //画椭圆形
let line = this.context.createLinearGradient(110, 500, 110, 600)
line.addColorStop(0, '#f00')
line.addColorStop(0.5, '#ff0')
line.addColorStop(1, '#f0f')
this.context.fillStyle = line
this.context.fillRect(110, 500, 100, 100) //渐变色
this.context.fillText('我是中华人民共和国的公民', 110, 500) //写字
let path = new Path2D();
path.moveTo(100, 100)
path.lineTo(100, 150)
path.lineTo(25, 150)
path.lineTo(175, 150)
path.lineTo(100, 150)
path.lineTo(100, 200)
path.lineTo(40, 200)
path.lineTo(160, 200)
path.lineTo(100, 200)
path.closePath()
this.context.strokeStyle = '#f00'
this.context.lineWidth = 5
this.context.stroke(path)
let path2 = new Path2D();
path2.moveTo(40, 215)
path2.lineTo(160, 215)
path2.lineTo(160, 335)
path2.lineTo(40, 335)
path2.lineTo(40, 215)
this.context.stroke(path2) //再次画个“吉”
})
}
.tabBar(this.TabBuild('画布', 3, $r('app.media.ic_hm_logo'), $r('app.media.icon')))
TabContent() {
Column() {
Text('原始尺寸Circle')
Circle({ width: 100, height: 100 }).fill(0x00c250)
Row({ space: 10 }) {
Column() {
Text('shape内放大的Circle')
Shape() {
Rect({ width: 100, height: 100 }).fill(0xf7f7f7)
Circle({ width: 150, height: 150 }).fill(0x00c250)
}.viewPort({ x: 0, y: 0, width: 100, height: 100 }) //根据这个视口与宽高比进行放大shape内的组件大小
.width(150)
.height(150)
Path()
.width(100)
.height(100)
.commands(`M${this._50} 0 L${this._50} ${this._50} L0 ${this._50} L${this._100} ${this._50} L${this._50} ${this._50} L${this._50} ${this._100} L${this._15} ${this._100} L${this._85} ${this._100} L${this._50} ${this._100} Z`)
.fillOpacity(0) //实心不填充颜色
.fill(Color.Red)
.stroke(Color.Red)
.strokeWidth(5)
Polyline()
.width(100)
.height(100)
.stroke(Color.Red)
.strokeWidth(5)
.points([[10, 10], [90, 10], [90, 90], [10, 90], [10, 10]])
.fillOpacity(0) //实心不填充颜色
}
}
}
}
.tabBar(this.TabBuild('我的', 4, $r('app.media.ic_hm_my_selected'), $r('app.media.ic_hm_my_normal')))
}
.vertical(false) //tabs垂直与横向设置
.scrollable(false) //禁止页面滑动
.barMode((BarMode.Fixed)) //Fixed 固定 Scrollable 可以滑动,当tab多时用
.onChange((index) => { //页面滑动监听
console.error(`this is ${index}`)
this.currentIndex = index;
// this.tabsController.changeIndex(this.currentIndex)
})
}
}
@Component
struct MyNavigation {
private arr: number[] = [1, 2, 3];
build() {
Column() {
Navigation() {
TextInput({ placeholder: '请输入...' })
.width('90%')
.height(40)
.backgroundColor('#ffffff')
.sharedTransition('input', { duration: 500, curve: Curve.Linear, type: SharedTransitionEffectType.Static })
Image($r('app.media.ic_hm_logo'))
.width(300)
.height(300)
.sharedTransition('sharedTransition', { duration: 500, curve: Curve.Linear })
.onClick(() => {
router.back()
})
List({ space: 12 }) {
ForEach(this.arr, item => {
ListItem() {
NavRouter() {
Text("NavRouter" + item)
.width('100%')
.height(72)
.backgroundColor(Color.White)
.borderRadius(36)
.fontSize(16)
.fontWeight(500)
.textAlign(TextAlign.Center)
NavDestination() {
Text(`NavDestinationContent${item}`)
}
.title(`NavDestinationTitle${item}`)
}
}
})
}
}
.title('主标题')
.mode(NavigationMode.Stack)
.titleMode(NavigationTitleMode.Mini)
.menus([
{ value: "", icon: './../../../resources/base/media/icon.png', action: () => {
} },
{ value: "", icon: './../../../resources/base/media/icon.png', action: () => {
} }
])
.toolBar({ items: [
{ value: 'func', icon: './../../../resources/base/media/icon.png', action: () => {
} },
{ value: 'func', icon: './../../../resources/base/media/icon.png', action: () => {
} }
] })
}
}
pageTransition() {
PageTransitionEnter({ type: RouteType.Push, duration: 1000, })
.slide(SlideEffect.Right)
PageTransitionEnter({ type: RouteType.Pop, duration: 1000, })
.slide(SlideEffect.Left)
PageTransitionExit({type: RouteType.Push, duration: 1000})
.slide(SlideEffect.Left)
PageTransitionExit({type: RouteType.Pop, duration: 1000})
.slide(SlideEffect.Right)
}
}