Launcher3拖拽分析(一)——Android13版本

Launcher3拖拽分析系列文章

第一章 拖拽事件的发起过程分析



前言

Launcher3比较重要特性的就是支持图标拖拽,本文记录下我学习的Android13版本的Launcher拖拽的过程,由于个人理解能力有限,如有错误或者补充之处,请大家悉数指教。

本文主要介绍launcher拖拽功能中几次事件是怎么来的,后面的文章再继续分析核心的拖拽(onDragOver和onDrop)


一、拖拽怎么发起的?

接触过AOSP的Launcher3项目的肯定不陌生,拖拽就是长按图标,等到workspace画面微缩后,就可以拖动该图标。如果遇到格子已经被占领,则挤走当前格子的图标(或者与其生成文件夹);如果当前格子没有被占领则可以放入这个位置;如果拖动到画面右侧边缘,则自动进入下一页。

在这里插入图片描述

那么这一切是怎么发生的呢?

二、明确几个对象

1、Workspace:主屏幕对应的布局,是直接添加到Launcher.xml中的布局对象
2、CellLayout:主屏幕中的每一页,其父布局就是Workspace,左右滑动屏幕,就是每一个CellLayout的变化过程,这个类中有很多处理拖拽相关方法。
3、ShortcutAndWidgetContainer:装载图标的容器(布局),其父布局是CellLayout。
4、BubbleTextView:launcher中的图标对象(单击、长按图标的实际载体)

其view树结构如下:
在这里插入图片描述
其中ShortcutAndWidgetContainer会被划分为m×n的格子区域,具体划分数量依据res/xml/device_profiles.xml中定义,大家可自行查看。

每个BubbTextView怎么显示在ShortcutAndWidgetContainer对应的格子的?

这就要依据BubbTextView携带的ItemInfo对象了,这个信息类封装了图标的一切信息:行列号、屏幕ID、图标名称、图标Drawable、Intent信息等等,这个信息对象非常重要!!!。ShortcutAndWidgetContainer的onMeasuew() 方法会对每个装载BubbTextView的格子指定大小(也是根据配置xml来的),在onLayout()方法中根据BubbTextView的行列号计算其在当前那个格子中。

因此,拖拽就是不断更新BubbTextView的行列号以及屏幕ID的过程,然后实时刷新。

除了上述几个用于显示的View对象,还有和拖拽相关的专用对象

1、DragLayer:拖拽图层,最顶层的View对象,其主要功能就是处理滑动事件,以及拖拽对象的动画效果。
其子View包含Workspace(主页)、PageIndicatorDots(分页指示器)、AllApp(更多应用界面、上拉弹出的抽屉页)、HotSeat(画面底部常驻图标区)…。具体大家可以查看res/layout/launcher.xml里面的内容,以及DragLayer类方法。

2、DragController:核心拖拽控制器基类,定义很多拖拽相关的公共方法,处理滑动事件等等,其子类重点关注LauncherDragController。

2、DropTarget:拖拽事件接口,在Workspace中有实现这个接口。其包含主要的拖拽事件:onDrop(拖拽结束松手的瞬间触发)、onDragEnter(进入拖拽触发)、onDragOver(拖拽过程中触发)、onDragExit(退出拖拽)。重点需要理解的就是onDragOver以及onDrop。

3、DragObject:DropTarget的内部类,顾名思义这个对象就是“拖拽对象”,其最重要的功能就是封装拖拽过程中的信息(数据结构)

4、DragView:BubbTextView的平替(他们携带的信息是一样的),因为BubbTextView的父布局是ShortcutAndWidgetContainer,如果拖拽到另一个ShortcutAndWidgetContainer是不允许的。所以创造了一个DragView来代替BubbTextView,这样拖动过程其实是拽着DragView动(原始的BubbTextView会被隐藏)

注意这里有个坑:如果在Launcher中替换了app的图标(BubbleTextView#applyIconAndLabel()方法中替换了FastBitmapDrawable),且图标资源是自适应的,但是拖拽开始时刻会创建DragView,DragView的图标还是原始的,从而发生拖拽时候被替换的图标被打回原型的情况。因此DragView的drawable资源也得跟着替换才行!!!

5、DraggableView:定义绘制预览、拖拽预览以及相关动画的接口,BubbleTextView中有相关的实现。

6、DragOptions:定义拖拽过程中的一些状态、行为信息(例如:是否正在拖拽,是否是键盘控制等等)。

二、拖拽触发的起点分析

1.BubbleTextView创建以及添加点击事件

代码如下(示例):

// Launcher.java中
public View createShortcut(ViewGroup parent, WorkspaceItemInfo info) {
		// 找到布局文件并创建对象
        BubbleTextView favorite = (BubbleTextView) LayoutInflater.from(parent.getContext())
                .inflate(R.layout.app_icon, parent, false);
        favorite.applyFromWorkspaceItem(info);
        // 添加单击事件,点击图标跳转对应的Activity
        favorite.setOnClickListener(getItemOnClickListener());
        // 添加焦点变更的回调
        favorite.setOnFocusChangeListener(mFocusHandler);
        return favorite;
}

上面只是BubbleTextView 创建过程,以及单击事件。那么长按事件在哪儿?

只要跟着Launcher#bindItems()步骤,就能找到WorkspaceLayoutManager#addInScreen()中

//WorkspaceLayoutManager.java
default void addInScreen(View child, int container, int screenId, int x, int y,
            int spanX, int spanY) {
            
		...
		...
		
		// 设置此视图是否应该为长按等事件提供触觉反馈。
		child.setHapticFeedbackEnabled(false);    

        // 这里也是一个关键点:设置当前child(BubbleTextView )的长按事件!!!
        child.setOnLongClickListener(getWorkspaceChildOnLongClickListener());
        if (child instanceof DropTarget) {
            onAddDropTarget((DropTarget) child);
        }

}

关键的来了,长按事件回调就写在getWorkspaceChildOnLongClickListener()方法中,接着跟代码,最终找到ItemLongClickListener#onWorkspaceItemLongClick():顾名思义就是桌面item的长按事件

// ItemLongClickListener.java
private static boolean onWorkspaceItemLongClick(View v) {
        if (v instanceof LauncherAppWidgetHostView) {
            TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick");
        } else {
            TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onWorkspaceItemLongClick");
        }
        // 拿到Launcher对象(activity)
        Launcher launcher = Launcher.getLauncher(v.getContext());
        // 一些条件过滤判断
        if (!canStartDrag(launcher)) return false;
        if (!launcher.isInState(NORMAL) && !launcher.isInState(OVERVIEW)) return false;
        if (!(v.getTag() instanceof ItemInfo)) return false;

        launcher.setWaitingForResult(null);
		
		// 核心来了:开始拖拽咯!!!
        beginDrag(v, launcher, (ItemInfo) v.getTag(), launcher.getDefaultWorkspaceDragOptions());
        return true;
}

接着看beginDrag()

// ItemLongClickListener.java
public static void beginDrag(View v, Launcher launcher, ItemInfo info,
            DragOptions dragOptions) {
		// 一些条件判断
        if (info.container >= 0) {
            Folder folder = Folder.getOpen(launcher);
            if (folder != null) {
                if (!folder.getIconsInReadingOrder().contains(v)) {
                    folder.close(true);
                } else {
                    folder.startDrag(v, dragOptions);
                    return;
                }
            }
        }

		// 封装了一个单元格信息的数据结构,类似于哈希表,key:BubbleTextView ,val:单元格信息(行列号...)
        CellLayout.CellInfo longClickCellInfo = new CellLayout.CellInfo(v, info);   
		
		// 开始调用Workspace中startDrag()方法 
        launcher.getWorkspace().startDrag(longClickCellInfo, dragOptions);
}

2.Workspace触发拖拽事件

接着看Workspace#startDrag()

// Workspace.java
public void startDrag(CellLayout.CellInfo cellInfo, DragOptions options) {
        // 拿出BubbleTextView
        View child = cellInfo.cell;

        mDragInfo = cellInfo;
        // 拖动第一瞬间,格子上原本的BubbleTextView设置为不可见,为DragView的出现做铺垫!!!
        child.setVisibility(INVISIBLE);   

        if (options.isAccessibleDrag) {
            mDragController.addDragListener(
                    new AccessibleDragListenerAdapter(this, WorkspaceAccessibilityHelper::new) {
                        @Override
                        protected void enableAccessibleDrag(boolean enable) {
                            super.enableAccessibleDrag(enable);
                            setEnableForLayout(mLauncher.getHotseat(), enable);
                        }
                    });
        }

        beginDragShared(child, this, options);
}

接着看beginDragShared(),头疼的来了~~

// Workspace.java
public DragView beginDragShared(View child, DraggableView draggableView, DragSource source,
            ItemInfo dragObject, DragPreviewProvider previewProvider, DragOptions dragOptions) {
		// 1 找到图标的缩放比
        float iconScale = 1f;
        if (child instanceof BubbleTextView) {
            Drawable icon = ((BubbleTextView) child).getIcon();
            if (icon instanceof FastBitmapDrawable) {
                iconScale = ((FastBitmapDrawable) icon).getAnimatedScale();
            }
        }

        // Clear the pressed state if necessary
        // 2、清除掉聚焦状态,以及按压背景信息
        child.clearFocus();
        child.setPressed(false);
        if (child instanceof BubbleTextView) {
            BubbleTextView icon = (BubbleTextView) child;
            icon.clearPressedBackground();
        }

		// 3、draggableView的创建
        if (draggableView == null && child instanceof DraggableView) {
            draggableView = (DraggableView) child;
        }

		// 4、构建拖拽相关的图标对象,原始BubbleTextView不是已经不可见了嘛(上面的代码中),
		// 因此这里给再造一个图标的Drawable
        final View contentView = previewProvider.getContentView();
        final float scale;
        // The draggable drawable follows the touch point around on the screen
        final Drawable drawable;
        if (contentView == null) {
            drawable = previewProvider.createDrawable();
            scale = previewProvider.getScaleAndPosition(drawable, mTempXY);
        } else {
            drawable = null;
            scale = previewProvider.getScaleAndPosition(contentView, mTempXY);
        }
		
		// 5、定义一些拖拽相关的bounds
        int halfPadding = previewProvider.previewPadding / 2;
        int dragLayerX = mTempXY[0];
        int dragLayerY = mTempXY[1];

        Point dragVisualizeOffset = null;
        Rect dragRect = new Rect();

        if (draggableView != null) {
            draggableView.getSourceVisualDragBounds(dragRect);
            dragLayerY += dragRect.top;
            dragVisualizeOffset = new Point(- halfPadding, halfPadding);
        }

		// 6、初始化mDragSourceInternal (ShortcutAndWidgetContainer对象)
        if (child.getParent() instanceof ShortcutAndWidgetContainer) {
            mDragSourceInternal = (ShortcutAndWidgetContainer) child.getParent();
        }

		// 7、初始化dragOptions相关的信息
        if (child instanceof BubbleTextView) {
            BubbleTextView btv = (BubbleTextView) child;
            if (!dragOptions.isAccessibleDrag) {
                dragOptions.preDragCondition = btv.startLongPressAction();
            }
            if (btv.isDisplaySearchResult()) {
                dragOptions.preDragEndScale = (float) mAllAppsIconSize / btv.getIconSize();
            }
        }

		// 8、关键的来了,上面的代码都是做拖拽前的准备工作,下面的mDragController.startDrag()才是关键
		// 创建DragView并开始拖拽!!!!!!
        final DragView dv;
        // 这两个判断就是看有么有drawable,反正最终都会调用startDrag()方法
        if (contentView instanceof View) {
            if (contentView instanceof LauncherAppWidgetHostView) {
                mDragController.addDragListener(new AppWidgetHostViewDragListener(mLauncher));
            }
            dv = mDragController.startDrag(
                    contentView,
                    draggableView,
                    dragLayerX,
                    dragLayerY,
                    source,
                    dragObject,
                    dragVisualizeOffset,
                    dragRect,
                    scale * iconScale,
                    scale,
                    dragOptions);
        } else {
            dv = mDragController.startDrag(
                    drawable,
                    draggableView,
                    dragLayerX,
                    dragLayerY,
                    source,
                    dragObject,
                    dragVisualizeOffset,
                    dragRect,
                    scale * iconScale,
                    scale,
                    dragOptions);
        }
        return dv;
    }

关键代码来到了dv = mDragController.startDrag() , 跟着代码跳到了: DragController#startDrag() --> LauncherDragController#startDrag。

其中LauncherDragController是DragController的子类之一,调试代码的时候发现最终调用的是LauncherDragController#startDrag()方法。

因此接着看LauncherDragController#startDrag()方法:

 @Override
   protected DragView startDrag(
           @Nullable Drawable drawable,
           @Nullable View view,
           DraggableView originalView,
           int dragLayerX,
           int dragLayerY,
           DragSource source,
           ItemInfo dragInfo,
           Point dragOffset,
           Rect dragRegion,
           float initialDragViewScale,
           float dragViewScaleOnDrop,
           DragOptions options) {
       if (TestProtocol.sDebugTracing) {
           Log.d(TestProtocol.NO_DROP_TARGET, "5");
       }
       if (PROFILE_DRAWING_DURING_DRAG) {
           android.os.Debug.startMethodTracing("Launcher");
       }

       mActivity.hideKeyboard(); // 隐藏键盘
       AbstractFloatingView.closeOpenViews(mActivity, false, TYPE_DISCOVERY_BOUNCE);

       Log.d("TAG", "startDrag: LauncherDragController 1");
       // 下面就是创建一些对象、变量
       mOptions = options;
       if (mOptions.simulatedDndStartPoint != null) {
           mLastTouch.x = mMotionDown.x = mOptions.simulatedDndStartPoint.x;
           mLastTouch.y = mMotionDown.y = mOptions.simulatedDndStartPoint.y;
       }

       final int registrationX = mMotionDown.x - dragLayerX;
       final int registrationY = mMotionDown.y - dragLayerY;

       final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
       final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;

       mLastDropTarget = null;

       mDragObject = new DropTarget.DragObject(mActivity.getApplicationContext());
       mDragObject.originalView = originalView;

       mIsInPreDrag = mOptions.preDragCondition != null
               && !mOptions.preDragCondition.shouldStartDrag(0);

       final Resources res = mActivity.getResources();
       final float scaleDps = mIsInPreDrag
               ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f;
       
       // 核心代码1:创建DragView 的过程
       final DragView dragView = mDragObject.dragView = drawable != null
               ?
               new LauncherDragView(
               mActivity, drawable, registrationX, registrationY, initialDragViewScale,
               dragViewScaleOnDrop, scaleDps)
               :
               new LauncherDragView(mActivity, view,
               view.getMeasuredWidth(), view.getMeasuredHeight(), registrationX, registrationY,
               initialDragViewScale, dragViewScaleOnDrop, scaleDps);
       // 给dragView赋予 itemInfo对象(itemInfo对象封装了图标的行列号、屏幕ID、图标的title、intent等等信息,后续拖拽过程中会拿这个对象出来用)
       dragView.setItemInfo(dragInfo);

       mDragObject.dragComplete = false;

       mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
       mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop);

	   // 核心代码2: 创建DragDriver对象,其内部有EventListener接口
       mDragDriver = DragDriver.create(this, mOptions, mFlingToDeleteHelper::recordMotionEvent);
       if (!mOptions.isAccessibleDrag) {
           mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
       }

		// 赋予一些变量,方便后续取值用
       mDragObject.dragSource = source;
       mDragObject.dragInfo = dragInfo;
       mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();

       if (dragOffset != null) {
           dragView.setDragVisualizeOffset(new Point(dragOffset));
       }
       if (dragRegion != null) {
           dragView.setDragRegion(new Rect(dragRegion));
       }

       mActivity.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
       dragView.show(mLastTouch.x, mLastTouch.y);
       mDistanceSinceScroll = 0;

       if (!mIsInPreDrag) {
           callOnDragStart();
       } else if (mOptions.preDragCondition != null) {
           mOptions.preDragCondition.onPreDragStart(mDragObject);
       }

       Log.d("TAG", "startDrag: LauncherDragController 2");
       // 非核心 处理一次的拖拽的移动事件(因为这个方法只触发一次,所以这里只走一次)
       handleMoveEvent(mLastTouch.x, mLastTouch.y);

       if (!mActivity.isTouchInProgress() && options.simulatedDndStartPoint == null) {
           // If it is an internal drag and the touch is already complete, cancel immediately
           MAIN_EXECUTOR.submit(this::cancelDrag);
       }
       return dragView;
   }

重点就在注释“核心代码2”这里,mDragDriver 被创建了,这个类包含一个EventListener接口,其内容如下所示:
在这里插入图片描述

并且DragController(及其子类)实现了这个接口:

在这里插入图片描述
根据我的理解:这个EventListener接口会在DragDriver#onDragEvent(DragEvent event)中被触发
在这里插入图片描述

至于这个event事件从哪儿来的,我就没继续往上层找了(主要是再上层的调用我也有点蒙蔽,工作时间有限也没继续深究,如果有朋友知道也请不吝赐教~~)。

既然这里触发了EventListener接口,而DragController实现了这个接口从而得到事件的响应,从而触发一系列拖拽事件:
onDragStart() --> onDragEnter() --> onDragOver() --> onDragExit() --> onDrop() --> onDragEnd()
在这里插入图片描述
至此拖拽的起点已经找到了


总结

本文主要介绍Android13的拖拽事件的起点分析,包括介绍了几个拖拽过程中用到的对象,以及长按事件触发的拖拽过程。

BubbtextView首先设置了长按监听事件,最终workspace触发了startDrag()方法(在这一过程中,把原始BubbtextView隐藏,创建了一个DragView来代替,同时杂七杂八的创建了很多变量、对象)。

关键的是创建一个DragDriver 对象,这个对象包含onDragEvent(),以及EventListener接口,并在onDragEvent中调用EventListener接口,换句话说调用了DragController的接口(DragController implements EventListener)从而引发了:

onDragStart() --> onDragEnter() --> onDragOver() --> onDragExit() --> onDrop() --> onDragEnd() 过程。

下一章,继续分析onDragOver() 事件发生了啥

最后由于本人能力有限,有些理解可能存在问题,如果有误欢迎大家指正~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值