在DragView的拖拽过程中,会查找拖拽中心mDragViewVisualCenter最近的Cell位置,并根据Cell位置上是否已经被应用图标控件占据,分为两种情况。
1.没有被应用图标占据,那么则进入拖拽轮廓的显示。
2.当已经被应用程序占据时,需要让Cell位置上的图标控件挪开,进行ReOrder重新排列。
今天我们就来浅析下第二种方式是如何排序的。当DragView被拖动时,会不断调用WorkSpace的onDragOver方法,在onDragOver中的如下一段代码中的else代码段即是当应用程序已经占据最近Cell时调用的方法。
//如果没有应用图标在
if (!nearestDropOccupied) {
//在onDragOver中不断被调用,下面就是显示轮廓的具体方法
mDragTargetLayout.visualizeDropLocation(child, mDragOutline,
(int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1],
mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false,
d.dragView.getDragVisualizeOffset(), d.dragView.getDragRegion());
}
//如果当前有应用图标在,而且之前的manageFolderFeedback没成立,那么就需要让mTargetCell挪位置
else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
&& !mReorderAlarm.alarmPending() && (mLastReorderX != mTargetCell[0] ||
mLastReorderY != mTargetCell[1])) {
// Otherwise, if we aren't adding to or creating a folder and there's no pending
// reorder, then we schedule a reorder
ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter,
minSpanX, minSpanY, item.spanX, item.spanY, d.dragView, child);
mReorderAlarm.setOnAlarmListener(listener);
mReorderAlarm.setAlarm(REORDER_TIMEOUT);
}
可以看见else方法中时调用了Luancher封装好的AlarmListener,此机制设置一个Alarm报铃时间,到时间就会走其中的onAlarm方法。所以我们直接去ReoderAlarmListener中的onAlarm去看看。
public void onAlarm(Alarm alarm) {
int[] resultSpan = new int[2];
//找出距离DragView的最近的Cell
mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
(int) mDragViewVisualCenter[1], spanX, spanY, mDragTargetLayout, mTargetCell);
//使用mLastReorderX,Y暂存最近Cell的X,Y值
mLastReorderX = mTargetCell[0];
mLastReorderY = mTargetCell[1];
//此处就是做动画,让出位置
mTargetCell = mDragTargetLayout.createArea((int) mDragViewVisualCenter[0],
(int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
child, mTargetCell, resultSpan, CellLayout.MODE_DRAG_OVER);
if (mTargetCell[0] < 0 || mTargetCell[1] < 0) {
mDragTargetLayout.revertTempState();
} else {
setDragMode(DRAG_MODE_REORDER);
}
boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY;
//显示轮廓
mDragTargetLayout.visualizeDropLocation(child, mDragOutline,
(int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1],
mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize,
dragView.getDragVisualizeOffset(), dragView.getDragRegion());
}
onAlarm做了如下几部操作:
1.找出距离当前DragView最近的Cell位置mTargetCell。
2.调用CellLayout的createArea方法做动画挪出mTargetCell位置。
3.调用visualizeDropLocation方法,在mTargetCell位置显示DragView的轮廓。
也就是说本文讨论的其实主要就是CellLayout的createArea方法。其内部负责找到挪动策略来空出mTargetCell位置,并且也负责了挪动动画,以及挪动完成之后的被挪动控件的抖动shake。
我们进入createArea方法:
int[] createArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY,
View dragView, int[] result, int resultSpan[], int mode) {
//找出最近区域
result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
if (resultSpan == null) {
resultSpan = new int[2];
}
//方向矢量
if ((mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL || mode == MODE_ACCEPT_DROP)
&& mPreviousReorderDirection[0] != INVALID_DIRECTION) {
mDirectionVector[0] = mPreviousReorderDirection[0];
mDirectionVector[1] = mPreviousReorderDirection[1];
// We reset this vector after drop
if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
mPreviousReorderDirection[0] = INVALID_DIRECTION;
mPreviousReorderDirection[1] = INVALID_DIRECTION;
}
} else {//MODE_DRAG_OVER
getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView, mDirectionVector);
mPreviousReorderDirection[0] = mDirectionVector[0];
mPreviousReorderDirection[1] = mDirectionVector[1];
}
ItemConfiguration swapSolution = simpleSwap(pixelX, pixelY, minSpanX, minSpanY,
spanX, spanY, mDirectionVector, dragView, true, new ItemConfiguration());
// We attempt the approach which doesn't shuffle views at all
ItemConfiguration noShuffleSolution = findConfigurationNoShuffle(pixelX, pixelY, minSpanX,
minSpanY, spanX, spanY, dragView, new ItemConfiguration());
ItemConfiguration finalSolution = null;
if (swapSolution.isSolution && swapSolution.area() >= noShuffleSolution.area()) {
finalSolution = swapSolution;
Log.i(TAG,"swapSolution:"+mDirectionVector);
} else if (noShuffleSolution.isSolution) {
finalSolution = noShuffleSolution;
Log.i(TAG,"noShuffleSolution"+mDirectionVector);
}
boolean foundSolution = true;
if (!DESTRUCTIVE_REORDER) {
//是否使用临时坐标mTmpCellX,mTmpCellY等,此处声明使用临时坐标
setUseTempCoords(true);
}
if (finalSolution != null) {
result[0] = finalSolution.dragViewX;
result[1] = finalSolution.dragViewY;
resultSpan[0] = finalSolution.dragViewSpanX;
resultSpan[1] = finalSolution.dragViewSpanY;
// If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
// committing anything or animating anything as we just want to determine if a solution
// exists
if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
if (!DESTRUCTIVE_REORDER) {
//
copySolutionToTempState(finalSolution, dragView);
}
setItemPlacementDirty(true);
//开始平移动画
animateItemsToSolution(finalSolution, dragView, mode == MODE_ON_DROP);
if (!DESTRUCTIVE_REORDER &&
(mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) {
//当up事件产生时走此处
commitTempPlacement();
completeAndClearReorderHintAnimations();
setItemPlacementDirty(false);
} else {
//开始shake动画过程
beginOrAdjustHintAnimations(finalSolution, dragView,
REORDER_ANIMATION_DURATION);
}
}
} else {
foundSolution = false;
result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1;
}
if ((mode == MODE_ON_DROP || !foundSolution) && !DESTRUCTIVE_REORDER) {
//是否使用临时坐标mTmpCellX,mTmpCellY等,此处声明不使用临时坐标
setUseTempCoords(false);
}
mShortcutsAndWidgets.requestLayout();
return result;
}
具体步骤如下:
1.找出距离DragView距离最近的Cell位置int result[]。
2.确定mDirectionVector。
3.找出挪动策略ItemConfiguration,据笔者观察,一般是swapSolution策略。
4.使用临时坐标。也就是CellLayout.LayoutParams中的成员变量mTmpCellX,mTmpCellY声明为可用。正是有如此声明我们才可以通过临时位置与ItemInfo的CellX,CellY位置是否相等来判断到底是那些Cell位置上的控件被挪动了,也可根据此判断来确定那些控件可以抖动shake。
5.调用copySolutionToTempState方法,用swapSolution过程中为每个Cell创建的CellAndSpan中的x,y赋值给Cell的临时位置mTmpCellX,mTmpCellY。
6.调用animateItemsToSolution做平移动画,其内部最终调用到如下代码。
public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration,
int delay, boolean permanent, boolean adjustOccupied) {
ShortcutAndWidgetContainer clc = getShortcutsAndWidgets();
boolean[][] occupied = mOccupied;
if (!permanent) {
occupied = mTmpOccupied;
}
if (clc.indexOfChild(child) != -1) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final ItemInfo info = (ItemInfo) child.getTag();
// We cancel any existing animations
if (mReorderAnimators.containsKey(lp)) {
mReorderAnimators.get(lp).cancel();
mReorderAnimators.remove(lp);
}
final int oldX = lp.x;
final int oldY = lp.y;
if (adjustOccupied) {
occupied[lp.cellX][lp.cellY] = false;
occupied[cellX][cellY] = true;
}
lp.isLockedToGrid = true;
if (permanent) {
lp.cellX = info.cellX = cellX;
lp.cellY = info.cellY = cellY;
} else {
lp.tmpCellX = cellX;
lp.tmpCellY = cellY;
}
clc.setupLp(lp);
lp.isLockedToGrid = false;
final int newX = lp.x;
final int newY = lp.y;
lp.x = oldX;
lp.y = oldY;
// 如果相等则说明当前Cell位置上的应用图标控件没有被挪动过
if (oldX == newX && oldY == newY) {
lp.isLockedToGrid = true;
return true;
}
ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f);
va.setDuration(duration);
mReorderAnimators.put(lp, va);
//通过改变x,y值达到平移效果
va.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float r = ((Float) animation.getAnimatedValue()).floatValue();
lp.x = (int) ((1 - r) * oldX + r * newX);
lp.y = (int) ((1 - r) * oldY + r * newY);
child.requestLayout();
}
});
va.addListener(new AnimatorListenerAdapter() {
boolean cancelled = false;
public void onAnimationEnd(Animator animation) {
// If the animation was cancelled, it means that another animation
// has interrupted this one, and we don't want to lock the item into
// place just yet.
if (!cancelled) {
lp.isLockedToGrid = true;
child.requestLayout();
}
if (mReorderAnimators.containsKey(lp)) {
mReorderAnimators.remove(lp);
}
}
public void onAnimationCancel(Animator animation) {
cancelled = true;
}
});
va.setStartDelay(delay);
va.start();
return true;
}
return false;
}
7.因为处于拖拽过程中,所以mode==MODE_DRAG_OVER,走到esle方法调用beginOrAdjustHintAnimations开始做抖动动画。
private void beginOrAdjustHintAnimations(ItemConfiguration solution, View dragView, int delay) {
int childCount = mShortcutsAndWidgets.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = mShortcutsAndWidgets.getChildAt(i);
if (child == dragView) continue;
CellAndSpan c = solution.map.get(child);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (c != null) {
//做shake动画
ReorderHintAnimation rha = new ReorderHintAnimation(child, lp.cellX, lp.cellY,
c.x, c.y, c.spanX, c.spanY);
rha.animate();
}
}
}
可以看见方法内部使用ReorderHintAnimation的animate来做抖动动画。既如下:
void animate() {
if (mShakeAnimators.containsKey(child)) {
ReorderHintAnimation oldAnimation = mShakeAnimators.get(child);
oldAnimation.cancel();
mShakeAnimators.remove(child);
if (finalDeltaX == 0 && finalDeltaY == 0) {
completeAnimationImmediately();
return;
}
}
//此处表示从构造参数传入的 lp.cellX, lp.cellY,c.x, c.y如果相同的话finalDeltaX与finalDeltaY
//就会为0,也即意味着此child没有被挪动,所以就不需要做抖动动画
if (finalDeltaX == 0 && finalDeltaY == 0) {
return;
}
//shake动画
ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f);
a = va;
va.setRepeatMode(ValueAnimator.REVERSE);
//抖动会一直反复持续
va.setRepeatCount(ValueAnimator.INFINITE);
va.setDuration(DURATION);
va.setStartDelay((int) (Math.random() * 60));
va.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float r = ((Float) animation.getAnimatedValue()).floatValue();
float x = r * finalDeltaX + (1 - r) * initDeltaX;
float y = r * finalDeltaY + (1 - r) * initDeltaY;
child.setTranslationX(x);
child.setTranslationY(y);
float s = r * finalScale + (1 - r) * initScale;
child.setScaleX(s);
child.setScaleY(s);
}
});
va.addListener(new AnimatorListenerAdapter() {
public void onAnimationRepeat(Animator animation) {
// We make sure to end only after a full period
initDeltaX = 0;
initDeltaY = 0;
initScale = getChildrenScale();
}
});
mShakeAnimators.put(child, this);
va.start();
}
8.设置临时位置变量不可用。