【鸿蒙开发教程】五分钟学会HarmonyOS中的 DragView

一、前言

随着春节假期结束各行各业复产复工,一年一度的春招也持续火热起来。最近,有招聘平台发布了《2024年春招市场行情周报(第一期)》。总体来说今年的就业市场还是人才饱和的状态,竞争会比较激烈。
但是,通过报告我们也能看到让人眼前一亮的信息,比如华为鸿蒙系统对应的人才市场就呈现出“供需两旺”的场景。
不久前华为已经宣布全新HarmonyOS NEXT 鸿蒙星河版将在今年秋天正式和消费者见面,并已经面向开发者开放申请。鸿蒙星河版会有更智能、更极致的原生体验,也标志着鸿蒙迈向其发展的第二阶段。
因此,对于鸿蒙生态建设而言,2024年可谓至关重要,而生态建设的前提,就是要有足够的开发人才。与之对应的,今年春招市场上与鸿蒙相关岗位和人才旺盛的热度,一方面反应了鸿蒙生态的逐渐壮大,另一方面也让人们对鸿蒙下一阶段的发展更具信心。

随着鸿蒙市场份额的不断提升,相应的岗位也会迎来一个爆发式的增长。这对于想要换赛道的程序员来说是一个非常好的消息,话说大家最近有想法转型鸿蒙开发吗?

在学习API9的时候就写了一个DragView,用于展示某个页面的悬浮可拖动的入口,特意丰富了许多的功能,今天分享给大家~。Demo基于API11。

二、思路

因为API本身就带有拖拽的手势,所以直接使用:PanGesture,根据拖拽返回的坐标,动态的更新DragView的position坐标。即可实现拖拽的功能。
除了拖拽,还需要的是从停留位置,吸附到某个位置。我们使用animateTo,结合坐标值即可完成很好的吸附效果。

三、准备容器

使用.position(this.curPosition)来控制拖拽的UI位置。dragContentBuilder方便自定义内容,组件的复用。

 @State private curPosition: Position = { x: 0, y: 0 };
 build() {
     Stack() {
         if (this.dragContentBuilder) {
             this.dragContentBuilder()
         } else {
             this.defDragView()
         }
     }
     )
     .position(this.curPosition)
     .onClick(this.onClickListener)
 }

四、边界

一般而言,拖拽的边界肯定是当前屏幕中的,但是如果需求需要限制在某个区域,或者需要规避一些位置。所以我们准备一个边界对象,来更好的管理拖拽的边界。

 boundArea: BoundArea = new BoundArea(0, 0, px2vp(display.getDefaultDisplaySync()
         .width), px2vp(display.getDefaultDisplaySync().height))
 export class BoundArea {
     readonly start: number = 0
     readonly end: number = 0
     readonly top: number = 0
     readonly bottom: number = 0
     readonly width: number = 0
     readonly height: number = 0
     readonly centerX: number = 0
     readonly centerY: number = 0
 ​
     constructor(start: number, top: number, end: number, bottom: number) {
         this.start = start
         this.top = top
         this.end = end
         this.bottom = bottom
         this.width = this.end - this.start
         this.height = this.bottom - this.top
         this.centerX = this.width / 2 + this.start
         this.centerY = this.height / 2 + this.top
     }
 }

boundArea默认使用了整个屏幕的坐标。

五、容器大小

因为具体的UI是从外部传入的,所以宽高不确定,需要计算。我们这里使用onAreaChange,绑定到容器上:

 .onAreaChange((oldValue: Area, newValue: Area) => {
     let height = newValue.height as number
     let width = newValue.width as number
     if ((this.dragHeight != height || this.dragWidth != width) && (height != 0 && width != 0)) {
         this.dragHeight = height
         this.dragWidth = width
     }
 })

可以看到,在容器发生改变的时候,我们保存它的宽高。

六、拖拽

拖拽手势使用起来还是很简单的:

 private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All });

direction决定了可以在哪个方向拖,我们显然需要所有方向。当然如果后续需要限制拖动方向,修改即可。
将拖动事件绑定到容器上:

 .gesture( // 绑定PanGesture事件,监听拖拽动作
     PanGesture(this.panOption)
         .onActionStart((event: GestureEvent) => {
             this.changePosition(event.offsetX, event.offsetY)
         })
         .onActionUpdate((event: GestureEvent) => {
             this.changePosition(event.offsetX, event.offsetY)
         })
         .onActionEnd((event: GestureEvent) => {
             this.endPosition = this.curPosition
             this.adsorbToEnd(this.endPosition.x, this.endPosition.y)
         })
 )

分别处理三个事件,onActionStart和onActionUpdate事件是独立的,但是逻辑一致所以全部使用this.changePosition(event.offsetX, event.offsetY)处理。

 private changePosition(offsetX: number, offsetY: number) {
     let targetX = this.endPosition.x + offsetX;
     let targetY = this.endPosition.y + offsetY;
     targetX = Math.max(this.boundArea.start, Math.min(targetX, this.boundArea.end - this.dragHeight));
     targetY = Math.max(this.boundArea.top, Math.min(targetY, this.boundArea.bottom - this.dragWidth));
     this.curPosition = { x: targetX, y: targetY };
 }

因为存在边界,所以我们需要限制curPosition的变化,在当前拖动的坐标和边界值之间取合理的值。因为容器存在宽高,所以我们需要考虑到其宽高。
当手指抬起的时候,需要做动画吸附:

 private adsorbToEnd(startX: number, startY: number) {
     let targetX = 0
     let targetY = 0
     if (startX <= (this.boundArea.centerX)) {
         targetX = this.boundArea.start + ((this.dragMargin.left ?? 0) as number)
     } else {
         targetX = this.boundArea.end - ((this.dragMargin.right ?? 0) as number) - this.dragWidth
     }
     let newTopBound = this.boundArea.top + ((this.dragMargin.top ?? 0) as number)
     let newBottomBound = this.boundArea.bottom - ((this.dragMargin.bottom ?? 0) as number) - this.dragWidth
     if (startY <= newTopBound) {
         targetY = newTopBound
     } else if (startY >= newBottomBound) {
         targetY = newBottomBound
     } else {
         targetY = startY
     }
     this.startMoveAnimateTo(targetX, targetY)
 }
 ​
 private startMoveAnimateTo(x: number, y: number) {
     animateTo({
         duration: 300,
         curve: Curve.Smooth, 
         iterations: 1, 
         playMode: PlayMode.Normal, 
         onFinish: () => {
             this.endPosition = this.curPosition
         }
     }, () => {
         this.curPosition = { x: x, y: y }
     })
 }

startX <= (this.boundArea.centerX)用于判断在边界的位置,根据位置来决定吸附到左边还是右边。计算出吸附的位置之后,只需要使用animateTo来触发this.curPosition的更新即可。
七、初始位置
如果不能控制一开始的显示位置,对于使用者的体验非常不好,所以我们可以新增一个参数Alignment来更改初始位置:

 dragAlign: Alignment = Alignment.BottomStart

可能还要微调位置,所以再加一个margin:

 dragMargin: Margin = {}

在onAreaChange的时候进行更新:

 .onAreaChange((oldValue: Area, newValue: Area) => {
     //.....
     if (this.isNotInit) {
         this.initAlign()
     }
 })
 ​
 private initAlign() {
     this.isNotInit = false
     let x = 0
     let y = 0
     let topMargin: number = (this.dragMargin.top ?? 0) as number
     let bottomMargin: number = (this.dragMargin.bottom ?? 0) as number
     let startMargin: number = (this.dragMargin.left ?? 0) as number
     let endMargin: number = (this.dragMargin.right ?? 0) as number
     switch (this.dragAlign) {
         case Alignment.Start:
             x = this.boundArea.start + startMargin
             break;
         case Alignment.Top:
             y = this.boundArea.top + topMargin
             break;
         case Alignment.End:
             x = this.boundArea.end - this.dragWidth - endMargin
             break;
         case Alignment.Bottom:
             y = this.boundArea.bottom - this.dragHeight - bottomMargin
             break;
         case Alignment.TopStart:
             x = this.boundArea.start + startMargin
             y = this.boundArea.top + topMargin
             break;
         case Alignment.BottomStart:
             x = this.boundArea.start + startMargin
             y = this.boundArea.bottom - this.dragHeight - bottomMargin
             break;
         case Alignment.BottomEnd:
             x = this.boundArea.end - this.dragWidth - endMargin
             y = this.boundArea.bottom - this.dragHeight - bottomMargin
             break;
         case Alignment.Center:
             x = this.boundArea.centerX - this.dragWidth / 2 + startMargin - endMargin
             y = this.boundArea.centerY - this.dragHeight / 2 + topMargin - bottomMargin
             break;
     }
     this.curPosition = { x: x, y: y }
     this.endPosition = this.curPosition
 }

只要稍微考虑容器宽高并计算下就好了。

八、使用

非常简单

 DragView({
     dragAlign: Alignment.Center,
     dragMargin: bothway(10),
     dragContentBuilder:this.defDragView()
 })
 ​
 @Builder
 defDragView() {
     Stack() {
         Text("拖我")
             .width(50)
             .height(50)
             .fontSize(15)
     }
     .shadow({
         radius: 1.5,
         color: "#80000000",
         offsetX: 0,
         offsetY: 1
     })
     .padding(18)
     .borderRadius(30)
     .backgroundColor(Color.White)
     .animation({ duration: 200, curve: Curve.Smooth })
 }

当然你想往里面塞任何东西都行~

九、总结

当然还有很多人需要跨页面的悬浮窗,这可以参考应用内消息通知,活用subWindow.moveWindowTo(0, 0);
因为我使用的是Navigation路由方案,所以放在顶层直接是跨页面的。
完整的代码:(懒得上传了,只有一个import,复制即用)

 import { display, Position } from '@kit.ArkUI';
 ​
 @Preview
 @Component
 export struct DragView {
     private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All });
     private endPosition: Position = { x: 0, y: 0 }
     private dragHeight: number = 0
     private dragWidth: number = 0
     private dragMargin: Margin = {}
     boundArea: BoundArea = new BoundArea(0, 0, px2vp(display.getDefaultDisplaySync()
         .width), px2vp(display.getDefaultDisplaySync().height))
     private isNotInit: boolean = true
     @State private curPosition: Position = { x: 0, y: 0 };
     dragAlign: Alignment = Alignment.BottomStart
     onClickListener?: (event: ClickEvent) => void
     @BuilderParam dragContentBuilder: CustomBuilder
 ​
     build() {
         Stack() {
             if (this.dragContentBuilder) {
                 this.dragContentBuilder()
             } else {
                 this.defDragView()
             }
         }
         .onAreaChange((oldValue: Area, newValue: Area) => {
             let height = newValue.height as number
             let width = newValue.width as number
             if ((this.dragHeight != height || this.dragWidth != width) && (height != 0 && width != 0)) {
                 this.dragHeight = height
                 this.dragWidth = width
             }
             if (this.isNotInit) {
                 this.initAlign()
             }
         })
         .gesture( // 绑定PanGesture事件,监听拖拽动作
             PanGesture(this.panOption)
                 .onActionStart((event: GestureEvent) => {
                     this.changePosition(event.offsetX, event.offsetY)
                 })
                 .onActionUpdate((event: GestureEvent) => {
                     this.changePosition(event.offsetX, event.offsetY)
                 })
                 .onActionEnd((event: GestureEvent) => {
                     this.endPosition = this.curPosition
                     this.adsorbToEnd(this.endPosition.x, this.endPosition.y)
                 })
         )
         .position(this.curPosition)
         .onClick(this.onClickListener)
     }
 ​
     private adsorbToEnd(startX: number, startY: number) {
         let targetX = 0
         let targetY = 0
         if (startX <= (this.boundArea.centerX)) {
             targetX = this.boundArea.start + ((this.dragMargin.left ?? 0) as number)
         } else {
             targetX = this.boundArea.end - ((this.dragMargin.right ?? 0) as number) - this.dragWidth
         }
         let newTopBound = this.boundArea.top + ((this.dragMargin.top ?? 0) as number)
         let newBottomBound = this.boundArea.bottom - ((this.dragMargin.bottom ?? 0) as number) - this.dragWidth
         if (startY <= newTopBound) {
             targetY = newTopBound
         } else if (startY >= newBottomBound) {
             targetY = newBottomBound
         } else {
             targetY = startY
         }
         this.startMoveAnimateTo(targetX, targetY)
     }
 ​
     private changePosition(offsetX: number, offsetY: number) {
         let targetX = this.endPosition.x + offsetX;
         let targetY = this.endPosition.y + offsetY;
 ​
         targetX = Math.max(this.boundArea.start, Math.min(targetX, this.boundArea.end - this.dragHeight));
         targetY = Math.max(this.boundArea.top, Math.min(targetY, this.boundArea.bottom - this.dragWidth));
 ​
         this.curPosition = { x: targetX, y: targetY };
     }
 ​
     private startMoveAnimateTo(x: number, y: number) {
         animateTo({
             duration: 300, // 动画时长
             curve: Curve.Smooth, // 动画曲线
             iterations: 1, // 播放次数
             playMode: PlayMode.Normal, // 动画模式
             onFinish: () => {
                 this.endPosition = this.curPosition
             }
         }, () => {
             this.curPosition = { x: x, y: y }
         })
     }
 ​
     private initAlign() {
         this.isNotInit = false
         let x = 0
         let y = 0
         let topMargin: number = (this.dragMargin.top ?? 0) as number
         let bottomMargin: number = (this.dragMargin.bottom ?? 0) as number
         let startMargin: number = (this.dragMargin.left ?? 0) as number
         let endMargin: number = (this.dragMargin.right ?? 0) as number
         switch (this.dragAlign) {
             case Alignment.Start:
                 x = this.boundArea.start + startMargin
                 break;
             case Alignment.Top:
                 y = this.boundArea.top + topMargin
                 break;
             case Alignment.End:
                 x = this.boundArea.end - this.dragWidth - endMargin
                 break;
             case Alignment.Bottom:
                 y = this.boundArea.bottom - this.dragHeight - bottomMargin
                 break;
             case Alignment.TopStart:
                 x = this.boundArea.start + startMargin
                 y = this.boundArea.top + topMargin
                 break;
             case Alignment.BottomStart:
                 x = this.boundArea.start + startMargin
                 y = this.boundArea.bottom - this.dragHeight - bottomMargin
                 break;
             case Alignment.BottomEnd:
                 x = this.boundArea.end - this.dragWidth - endMargin
                 y = this.boundArea.bottom - this.dragHeight - bottomMargin
                 break;
             case Alignment.Center:
                 x = this.boundArea.centerX - this.dragWidth / 2 + startMargin - endMargin
                 y = this.boundArea.centerY - this.dragHeight / 2 + topMargin - bottomMargin
                 break;
         }
 ​
         this.curPosition = { x: x, y: y }
         this.endPosition = this.curPosition
     }
 ​
     @Builder
     defDragView() {
         Stack()
             .width(100)
             .height(100)
             .backgroundColor(Color.Orange)
     }
 }
 ​
 export class BoundArea {
     readonly start: number = 0
     readonly end: number = 0
     readonly top: number = 0
     readonly bottom: number = 0
     readonly width: number = 0
     readonly height: number = 0
     readonly centerX: number = 0
     readonly centerY: number = 0
 ​
     constructor(start: number, top: number, end: number, bottom: number) {
         this.start = start
         this.top = top
         this.end = end
         this.bottom = bottom
         this.width = this.end - this.start
         this.height = this.bottom - this.top
         this.centerX = this.width / 2 + this.start
         this.centerY = this.height / 2 + this.top
     }
 }

总结

总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。随着鸿蒙的不断发展以及国家的大力支持,未来鸿蒙职位肯定会迎来一个大的爆发,只有积极应对变化,不断学习和提升自己,我们才能在这个变革的时代中立于不败之地。

在这里插入图片描述

  • 24
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
package com.blog.dragview; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.os.Bundle; import android.util.Log; import com.blog.R; import org.json.JSONArray; import org.json.JSONException; import java.util.List; public class MainActivity extends Activity{ private DrawingView _view; @Override protected void onCreate(Bundle savedInstanceState) { _view = new DrawingView(this); super.onCreate(savedInstanceState); setContentView(_view); Bitmap bmp1 = BitmapFactory.decodeResource(getResources(), R.drawable.hengzhegou); Bitmap bmp2 = BitmapFactory.decodeResource(getResources(),R.drawable.pie); CustomBitmap customBitmap1 = new CustomBitmap(bmp1); CustomBitmap customBitmap2 = new CustomBitmap(bmp2); customBitmap1.setId(1); customBitmap2.setId(2); if (getSavedMatrix(1) != null){ Log.e("tag", "matrix 1 is not null"); customBitmap1.setMatrix(getSavedMatrix(1)); } if (getSavedMatrix(2) != null){ Log.e("tag", "matrix 2 is not null"); customBitmap2.setMatrix(getSavedMatrix(2)); } _view.addBitmap(customBitmap1); _view.addBitmap(customBitmap2); } //����matrix private void saveMatrix(CustomBitmap customBitmap){ Log.e("tag", "save matrix" + customBitmap.getId()); SharedPreferences.Editor editor = getSharedPreferences("matrix", Context.MODE_PRIVATE).edit(); Matrix matrix = customBitmap.matrix; float[] values = new float[9]; matrix.getValues(values); JSONArray array = new JSONArray(); for (float value:values){ try { array.put(value); } catch (JSONException e) { e.printStackTrace(); } } editor.putString(String.valueOf(customBitmap.getId()), array.toString()); editor.commit(); Log.e("tag", "save matrix id:" + customBitmap.getId() + "---------"+values[Matrix.MPERSP_0] + " , " + values[Matrix.MPERSP_1] + " , " + values[Matrix.MPERSP_2] + " , " + values[Matrix.MSCALE_X] + " , " + values[Matrix.MSCALE_Y] + " , " + values[Matrix.MSKEW_X] + " , " + values[Matrix.MSKEW_Y] + " , " +values[Matrix.MTRANS_X] + " , " + values[Matrix.MTRANS_Y]); } //��ȡmatrix private Matrix getSavedMatrix(int id){ SharedPreferences sp = getSharedPreferences("matrix", Context.MODE_PRIVATE); String result = sp.getString(String.valueOf(id), null); if (result != null){ float[] values = new float[9]; Matrix matrix = new Matrix(); try { JSONArray array = new JSONArray(result); for (int i = 0; i < array.length(); i++) { values[i] = Float.valueOf(String.valueOf(array.getDouble(i))); } matrix.setValues(values); } catch (JSONException e) { e.printStackTrace(); } Log.e("tag", "get matrix id:" + id + "---------"+values[Matrix.MPERSP_0] + " , " + values[Matrix.MPERSP_1] + " , " + values[Matrix.MPERSP_2] + " , " + values[Matrix.MSCALE_X] + " , " + values[Matrix.MSCALE_Y] + " , " + values[Matrix.MSKEW_X] + " , " + values[Matrix.MSKEW_Y] + " , " +values[Matrix.MTRANS_X] + " , " + values[Matrix.MTRANS_Y]); return matrix ; } return null; } @Override public void finish() { List<CustomBitmap> list = _view.getViews(); for (CustomBitmap customBitmap:list){ saveMatrix(customBitmap); } super.finish(); } }
06-05
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值