HarmonyOS鸿蒙Next中应用拖拽能力适配

HarmonyOS鸿蒙Next中应用拖拽能力适配

应用拖拽能力适配

一.业务简介

拖拽作为基本操作,具有直观、便捷等优点,当前HarmonyOS3.0上,发布了超级中转站功能,应用拖拽起的数据可以暂存至超级中转站中,用户也可以从超级中转站中将暂存的数据拖拽至其他应用。

因此,对于应用来说,可接入拖拽能力,实现数据的便捷分享与接入。

二.拖拽数据构造

1. 拖拽数据结构

拖拽数据使用ClipData进行传递,ClipData有内部类Clipdata.Item,一个ClipData中可包含一个或多个Item。

图片

ClipData构造函数有:

ClipData(CharSequence label, String[] mimeTypes, ClipData.Item item)

ClipData(ClipDescription description, ClipData.Item item)

ClipData(ClipData other)

除第三个构造函数为从另一个ClipData创建一个拷贝外,其余均是从构造ClipData.Item开始。

ClipData.Item中包含4种数据,分为:Text、Intent、Uri以及HtmlText;

ClipDescription中包含对拖拽数据的描述,构造函数为:

ClipDescription(CharSequence label, String[] mimeTypes)

Label可自定义用于标识拖拽数据,如标记数据来源App等;mimeTypes用于描述拖拽数据中的类型,与Item中的数据类型对应的,有四种类型:MIMETYPE_TEXT_PLAIN、MIMETYPE_TEXT_INTENT、MIMETYPE_TEXT_URILIST和MIMETYPE_TEXT_HTML,以及未知类型:MIMETYPE_UNKNOWN。

常用拖拽数据示例

文本数据拖拽示例代码:
private boolean startTextDrag() {
    View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(mTextView);

    ClipDescription clipDescription = new ClipDescription("label", new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN});

    ClipData clipData = new ClipData(clipDescription, new ClipData.Item(mTextView.getText()));

    mTextView.startDragAndDrop(clipData, shadowBuilder, null, View.DRAG_FLAG_GLOBAL); 
    return true;
}
图片、文件等数据拖拽示例代码:
private boolean startFileDrag() {
    View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(mDragView);

    File filePath = new File(getApplicationContext().getFilesDir(), "drag");
    File file = new File(filePath, "drag.jpg");
    Uri uri = FileProvider.getUriForFile(getApplicationContext(), "com.example.app.fileprovider", file);

    ClipDescription clipDescription = new ClipDescription("label", new String[]{ClipDescription.MIMETYPE_TEXT_URILIST});
    ClipData clipData = new ClipData(clipDescription, new ClipData.Item(uri)); // 构造Uri类型的ClipData

    mDragView.startDragAndDrop(clipData, shadowBuilder, null, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ);
    return true;
}
卡片类型数据拖拽:

图片 图片

示例代码:

private boolean startCardDrag(View view) {
    Intent intent = new Intent();

    intent.putExtra("Title", mTitle);    // 标题信息

    intent.putExtra("Description", mDescription);  // 描述信息

    intent.putExtra("LinkUrl", mLinkUrl);    // 链接地址

    intent.putExtra("AppName", "淘宝");    // 来源应用

    Drawable thumbResource = ContextCompat.getDrawable(this, R.drawable.ic_thumb_icon);
    BitmapDrawable thumbIcon = (BitmapDrawable) thumbResource;
    if (thumbIcon == null) {
        return false;
    }

    Bitmap thumbIconBitmap = thumbIcon.getBitmap();   // 生成拖入卡片缩略图bitmap

    // 缩略图不能过大,过大需进行压缩
    thumbIconBitmap = Bitmap.createScaledBitmap(thumbIconBitmap, 50, 50, true);
    intent.putExtra("ThumbData", thumbIconBitmap);

    Drawable resource = ContextCompat.getDrawable(this, R.drawable.ic_app_icon);    // 来源应用图标
    BitmapDrawable appIcon = (BitmapDrawable) resource;
    if (appIcon == null) {
        return false;
    }

    final Bitmap appIconBitmap = appIcon.getBitmap();

    intent.putExtra("AppIcon", appIconBitmap);   // 设置来源应用图标

    intent.putExtra("ContentType", 1);    // 设置为1,卡片类型

    ClipData.Item item = new ClipData.Item(mTitle + "\n" + mDescription + "\n" + mLinkUrl, null, intent, null);

    mMimeTypes.add(0, "text/vnd.android.intent");   // 设置为intent类型

    ClipDescription clipDescription = new ClipDescription("HwDragDropLabel", mMimeTypes.toArray(new String[0]));

    ClipData clipData = new ClipData(clipDescription, item);    // 构造拖拽数据ClipData

    View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);    // 设置拖拽浮层

    view.startDragAndDrop(clipData, shadowBuilder, null, View.DRAG_FLAG_GLOBAL);
    return true;   // 发起拖拽
}

三.应用拖出能力适配

拖拽行为一般与长按操作进行绑定,即通过长按触发拖拽。

通过长按触发拖拽可使用两种方案:

  1. 使用系统默认长按逻辑

对于需要提供拖拽能力的控件,通过添加长按系统默认长按监听setOnLongClickListener(callback())来对长按事件进行响应,callback()中进行上述DragShadowBuilder以及ClipData的构造,并最终通过startDragAndDrop()发起拖拽。

以TextView为例,mTextView为一个TextView实例,示例代码如下:

private void initLongClickListener() {
    if (mTextView == null) {
        return;
    }

    mTextView.setOnLongClickListener(view -> startTextDrag());  // 设置长按监听
}

private boolean startTextDrag() {
    View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(mTextView);
    ClipDescription clipDescription = new ClipDescription("label", new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN});
    ClipData clipData = new ClipData(clipDescription, new ClipData.Item(mTextView.getText()));

    mTextView.startDragAndDrop(clipData, shadowBuilder, null, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ);
    return true;
}
  1. 使用自定义长按逻辑

对于部分应用而言,长按本身已经绑定了某些操作,如弹出浮窗等,此时无法通过系统默认长按监听实现拖拽,因此可以进行自定义长按逻辑实现。

对于需要进行拖拽的控件,需在控件中重写dispatchDragEvent(MotionEvent event)方法,重写方法后,可以在控件中收到分发的MotionEvent事件。

MotionEvent事件中,常用事件为ACTION_DOWN、ACTION_MOVE、ACTION_UP和ACTION_CANCEL,其中对于判断长按行为最关键的是ACTION_DOWN和ACTION_MOVE事件。

当首次手指按压屏幕时,会下发ACTION_DOWN事件,当手指移动时,会下发ACTION_MOVE事件,由于手指按压会有轻微移动,实际上会不断下发ACTION_MOVE,因此可通过DOWN和连续的MOVE事件对长按行为进行检测:

图片

长按的判断需结合位置和时间两个元素,通过长按事件响应不同的操作;位置判断用于减少误判,由于MotionEvent事件中携带了事件的位置信息,因此可以通过事件坐标的变化来判断。

通过:

private final int mTouchSlop = ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop()

可以获取系统的滑动常量,即手指移动阈值,当手指移动的距离小于此阈值时,判断为持续长按;当手指移动的距离大于此阈值时,判断为手指滑动。

示例代码如下:

private long mStartPressTime = 0;    // 记录首次按压的时间

private long mLongPressDuration = 0;     // 记录长按时长

private double mXPos = 0.0;    // 记录首次x坐标信息

private double mYPos = 0.0;    // 记录首次y坐标信息

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mXPos = event.getRawX();     // DOWN事件时,记录首次坐标信
            mYPos = event.getRawY();
            mStartPressTime = event.getEventTime();     // 记录首次按压时间
            return true;
        case MotionEvent.ACTION_MOVE:
            double currentX = event.getRawX();      // MOVE时间,记录MOVE坐标
            double currentY = event.getRawY();
            
            // 更新长按时长
            mLongPressDuration = event.getEventTime() - mStartPressTime;

            // 判断当前是否是长按(根据坐标变化),且判断时长是否满足条件
            if (Math.abs(currentX - mXPos) <= mTouchSlop && Math.abs(currentY - mYPos) <= mTouchSlop 
                && mLongPressDuration > 1000) {
                return startDrag();     // 进行指定操作,如:拖拽
            }
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:    // 其他事件重置长按发起时间
            mStartPressTime = 0;
    }
    return super.dispatchTouchEvent(event);
}

根据事件的时间长短,可以区分不同时长的长按,如:500ms弹出PopupMenu,1000ms收起PopupMenu并发起拖拽,核心逻辑在重写的dispatchTouchEvent()的ACTION_MOVE事件中,示例代码如下:

case MotionEvent.ACTION_MOVE:
    double currentX = event.getRawX();      // MOVE时间,记录MOVE坐标
    double currentY = event.getRawY();
    
    // 更新长按时长
    mLongPressDuration = event.getEventTime() - mStartPressTime;

    // 判断是否长按(根据坐标变化),且判断时长和PopupMenu是否显示
    if (Math.abs(currentX - mXPos) <= mTouchSlop && Math.abs(currentY - mYPos) <= mTouchSlop 
        && mLongPressDuration > 500 && !isPopShow) {
        showPopup();    // 进行指定操作:弹出PopupMenu
        isPopShow = true;     // 可写入showPopup()方法中
    }

    // 当长按时长大于1000ms时
    if (Math.abs(currentX - mXPos) <= mTouchSlop && Math.abs(currentY - mYPos) <= mTouchSlop && mLongPressDuration > 1000) {     
        if (isPopShow) {
            hidePopup();     // 收起PopupMenu
            isPopShow = false;        // 可写入hidePopup()方法中
        }
        startDrag();      // 开始拖拽
    }

    break;

由此,可以在原有应用长按逻辑基础上,新增对于长按拖拽的适配。

四.应用拖入能力适配

对于应用来说,当提供了数据拖出能力后,相应的应当提供接收拖入数据的能力,以保证用户体验的一致性。

接收拖入数据,主要在于对控件注册拖拽监听,即setOnDragListener(callback()),注册拖拽监听后,应用则能够接收到拖拽事件DragEvent,DragEvent由系统侧下发,应用不需关注其具体实现,在注册拖拽监听后,在回调函数中关注DragEvent.ACTION_DROP事件。

常用的DragEvent事件有ACTION_DRAG_STARTED、ACTION_DRAG_ENTERED、ACTION_DRAG_LOCATION、ACTION_DRAG_EXITED、ACTION_DRAG_ENDED和ACTION_DROP,分别对应拖拽的发起、拖拽内容进入控件、拖拽内容在空间内移动、拖拽移出控件、拖拽结束和拖拽drop。

在ACTION_DROP中,可以从拖拽事件中获取到拖拽数据ClipData,从而获取到拖拽的内容,应用获取拖拽数据后,可以自行对拖拽数据进行处理,示例代码如下:

private void setListeners() {
    // 注册拖拽监听
    mDropView.setOnDragListener((view, dragEvent) -> canHandleDrag(dragEvent));
}

private boolean canHandleDrag(DragEvent dragEvent) {
    if (dragEvent == null) {
        return false;
    }

    int action = dragEvent.getAction();
    switch (action) {
        case DragEvent.ACTION_DROP:    // 当收到ACTION_DROP事件时,处理数据
            return hasHandleDrop(dragEvent);
        default:
            // todo:处理其他拖拽事件
    }
    return true;
}

private boolean hasHandleDrop(DragEvent dragEvent) {
    ClipData clipData = dragEvent.getClipData();      // 从拖拽事件中获取数据
    if (clipData == null || clipData.getItemCount() == 0) {
        return false;
    }
    // todo:对数据做进一步处理
}

更多关于HarmonyOS鸿蒙Next中应用拖拽能力适配的实战教程也可以访问 https://www.itying.com/category-93-b0.html

4 回复

666

更多关于HarmonyOS鸿蒙Next中应用拖拽能力适配的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


感谢分享

在HarmonyOS鸿蒙Next中,应用拖拽能力适配主要依赖于DragEventDragListener接口。开发者需在布局文件中为可拖拽组件设置onDragListener,并在onDrag方法中处理拖拽事件。通过DragEvent.ACTION_DRAG_STARTEDDragEvent.ACTION_DRAG_ENTERED等事件类型,实现拖拽开始、进入、离开等逻辑。同时,使用ClipData传递拖拽数据,确保数据在拖拽过程中的一致性。适配时需注意不同设备的屏幕尺寸和分辨率,确保拖拽体验的流畅性和一致性。

回到顶部