onestep长按图标拖动功能代码分析
文章结构
- 长按图标触发拖动
- 触摸事件派发,拖动图标处理
- 触摸事件派发,手指松开后处理
长按图标触发拖动
普通模式下,长按侧边栏图标,开始拖动图标的处理
public class SidebarListView extends ListView {
public SidebarListView(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mDivider = LayoutInflater.from(context).inflate(R.layout.sidebar_view_divider, null);
//设置长按事件监听器
setOnItemLongClickListener(mOnLongClickListener);
}
private AdapterView.OnItemLongClickListener mOnLongClickListener = new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) {
... ...
//记录下拖动的条目
mDraggedItem = (SidebarItem) SidebarListView.this.getAdapter().getItem(position);
mDragPosition = position;
//Drawable icon = mDraggedItem.getAvatar();
Drawable icon = new BitmapDrawable(mContext.getResources(), BitmapUtils.drawableToBitmap(mDraggedItem.getAvatar()));
//开始拖动,由侧边栏根视图开始处理,传入icon图像信息过去,后面用它画出来,跟随你的拖动显示
SidebarController.getInstance(mContext).getSidebarRootView().startDrag(icon, view, viewLoc);
//设置当前拖动的列表视图实例为此列表,因为后面需要处理图标移动,占位,挪位置的问题
mSideView.setDraggedList(SidebarListView.this);
//原图标消失,并触发视图树重新layout,从而前面getSidebarRootView().startDrag()里增加的视图layout监听器被回调
view.setVisibility(View.INVISIBLE);
return false;
}
};
SidebarRootView的处理:
public class SidebarRootView extends FrameLayout {
//具体图标控件长按触发拖拽事件,这里,根视图负责开始处理
public void startDrag(Drawable icon, View view, int[] loc) {
if(mDragging || mShowDragViewWhenRelayout != null){
return ;
}
//增加视图树layout监听器,在上一步的图标OnLongClick()处理中,原图标setVisibility(INVISIBLE)后触发视图树重绘,
//从而回调此监听器
mShowDragViewWhenRelayout = new ShowDragViewWhenRelayout(icon, view, loc);
getViewTreeObserver().addOnGlobalLayoutListener(mShowDragViewWhenRelayout);
//set sidebar to full screen
//把SideView窗口全屏,是因为要在底部显示个垃圾桶,你可以拖过去做图标删除的动作
SidebarController.getInstance(mContext).updateDragWindow(true);
}
private class ShowDragViewWhenRelayout implements ViewTreeObserver.OnGlobalLayoutListener {
...
@Override
public void onGlobalLayout() {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
//只有这个地方设置mDragging为true,也就是唯一触发的点就是长按图标,然后dispatchTouchEvent()中的拖动处理就起作用了。
mDragging = true;
//拖动的dragview的显示、处理,包括位置更新、最后放手时,找到合适位置或删除。
//把拖拽图标显示出来,构造,调用显示函数,在构造时,又同样增加了layout变化监听器,见下
mDragView = new DragView(mContext, iconOrig, mListViewItem, mLoc);
mDragView.showView();//触发构造函数中的layout监听器被回调,见下
//显示垃圾桶
mTrash.trashAppearWithAnim();
//回收监听器实例
mShowDragViewWhenRelayout = null;
}
}
}
拖动时的拖动图标处理类DragView的处理:
public class DragView {
public View mListViewItem;
private Drawable mIcon;
public final View mView;
//构造,把原来的图标、view传进来,后面从view中获得当前开始拖动的位置,把待显示的伪图标,准备好,show的时候,才加到父视图里
//增加了layout监听器
public DragView(Context context, Drawable icon, View view, int[] loc) {
//这是拖动的那个假view的layout
mView = LayoutInflater.from(context).inflate(R.layout.drag_view, null);
mIcon = icon;//被拖动的图标icon
mListViewItem = view;
mDragViewIcon = (ImageView) mView.findViewById(R.id.drag_view_icon);
mDragViewIcon.setBackground(mIcon);//被拖动的图标icon,被重新设为拖动时图标
mBubbleText = (TextView) mView.findViewById(R.id.drag_view_bubble_text);//跟随拖动图标的图标名称
mBubbleText.setText(getDisplayName());
//当show()被调用,通过addView()添加拖动图标到父视图中,触发视图树变化,回调此监听器,才让拖动图标可见
mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
//拖动图标的名称,给个动画,逐渐显示出来
Anim bubbleAlpha = new Anim(mBubbleText, Anim.TRANSPARENT, 100, Anim.CUBIC_OUT, new Vector3f(), new Vector3f(0, 0, 1));
bubbleAlpha.start();
int[] screenLoc = new int[2];
mListViewItem.getLocationOnScreen(screenLoc);
int realIconWidth = mListViewItem.getWidth();
int realIconHeight = mListViewItem.getHeight();
int deltaX = mView.getWidth() / 2 - realIconWidth / 2;
int deltaY = mDragViewIcon.getHeight() / 2 - realIconHeight / 2;
int x = screenLoc[0] - deltaX;
int y = screenLoc[1] - mBubbleText.getHeight() - deltaY;
mView.setTranslationX(x);
mView.setTranslationY(y);
//设置显示,这时,拖拽图标真正显示出来,真是千呼万唤始出来
mView.setVisibility(View.VISIBLE);
}
});
}
//用于拖动显示的伪图标dragview先添加到父视图中,真正显示可见却是在构造函数中的ViewTreeObserver.OnGlobalLayoutListener监听器里
public void showView() {
//用于拖动显示的伪图标,先隐藏
mView.setVisibility(View.INVISIBLE);
//用于拖动显示的伪图标dragview先添加到侧边栏视图中,就你正在拖动的那个伪图标
//因为DragView是内部类,所以可以调用外围类的方法,这里addView()其实就是SidebarRootView.addView()
//这一句触发视图树变化,因此,构造函数中的ViewTreeObserver.OnGlobalLayoutListener监听器将被回调
addView(mView);
}
}
触摸事件派发,拖动图标处理
Touch事件从ViewRootImpl派发下来,首先依次派发到三个根视图,即SidebarRootView,ContentView和TopView
但可以看到,只有TopView和SidebarRootView重写了dispatchTouchEvent,且只有SidebarRootView满足条件并处理了,
显然,此时长按拖动事件,本应该由侧边栏相关视图类来处理。
public class SidebarRootView extends FrameLayout {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {//触摸事件由此开始,触摸1、顶层处理,拖放处理
//处理触摸,拖动图标,因为在长按图标时,前面回调到startDrag时,就设置为true,即Dragging状态了
if (mDragging) {
precessTouch(ev);//拖动状态下,所有事件由它处理
return true;//返回true,截断触摸事件,不再向下派发
}
return super.dispatchTouchEvent(ev);
}
//6-7、处理触摸,拖动图标
private void precessTouch(MotionEvent event) {
...
switch (action) {
case MotionEvent.ACTION_DOWN : {
if (ENABLE_TOUCH_LOG) log.error("ACTION_DOWN");
break;
}
//触摸放开,处理拖动结果,两种情况,垃圾桶和新位置
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP : {
if (ENABLE_TOUCH_LOG) log.error("ACTION_UP");
//情况一:垃圾桶
if (mTrash.dragObjectUpOnUp(x, y, mDragView)) {
//handle uninstall
} else {//情况二,侧边栏放下,新位置
dropDrag();
mTrash.trashDisappearWithAnim(null);//垃圾桶,动画退出
}
if (mDragView != null) {//让DragScrollView取消滚动处理
// we need action_update to stop scroll !
mSideView.dragObjectMove(event, eventTime);
}
break;
}
//DragView拖动,更新显示
case MotionEvent.ACTION_MOVE : {
if (ENABLE_TOUCH_LOG) log.error("ACTION_MOVE");
if (mDragView != null) {
mDragView.move(x, y);//拖动图标移动
mSideView.dragObjectMove(event, eventTime);//侧栏边当前列表,要判断占位,挪动图标
mTrash.dragObjectMoveTo(x, y);//垃圾桶,要判断是否拖进来,根据触摸位置区域,垃圾桶要冒出来或者收回去
}
break;
}
case MotionEvent.ACTION_SCROLL : {//忽略,不做处理。ACTION_MOVE事件传递给了DragScrollView去处理了,但貌似结果也是没滚动
break;
}
}
}
//DragView对拖动触摸事件ACTION_MOVE的处理,更新位置显示
class DragView{
public void move(float touchX, float touchY) {
int x = (int) (touchX - mView.getWidth() / 2);
int y = (int) (touchY - mDragViewIcon.getHeight() / 2 - mBubbleText.getHeight());
//直接根据触摸点,移动DragView伪图标的位置
mView.setTranslationX(x);
mView.setTranslationY(y);
}
}
}
SideView对拖动触摸事件ACTION_MOVE的处理:
public class SideView extends RelativeLayout {
public void dragObjectMove(MotionEvent event, long eventTime) {
//长按图标时,mDraggedListView就设置了的,也是唯一设置的地方
//也就是当前拖动的列表,要处理图标占位、挪位的问题,见下
mDraggedListView.dragObjectMove((int)(event.getRawX()), (int)(event.getRawY()));
}
}
当前mDraggedListView,即SidebarListView对拖动触摸事件ACTION_MOVE的处理:
public class SidebarListView extends ListView {
public void dragObjectMove(int rawX, int rawY) {
if (Utils.inArea(rawX, rawY, this)) {//图标在此列表区域内时,需要处理
int count = getAdapter().getCount() - getHeaderViewsCount() - getFooterViewsCount();
if (count > 0) {//列表里图标数不为0
...
int[] localLoc = convertToLocalCoordinate(rawX, rawY, drawingRect);
int subViewHeight = drawingRect.bottom / getChildCount();
//获得当前触摸点在列表中的条目位置
int position = localLoc[1] / subViewHeight;
//在此函数处理
pointToNewPositionWithAnim(position);
}
}
}
//根据拖动图标的位置,去做图标位置的调整
private void pointToNewPositionWithAnim(int position) {
int headViewCount = getHeaderViewsCount();
int count = getCount() - getFooterViewsCount() - headViewCount;
int begin = headViewCount;
int end = begin + count;
// check invisible count
int invisibleViewCount = 0;
for (int i = begin; i < end; i++) {
View view = getChildAt(i);
if (view.getVisibility() != View.VISIBLE) {
invisibleViewCount++;//不可见的子视图图标的数量
}
}
mDragPosition = position;//记录这一次拖动图标位置
position -= this.getHeaderViewsCount();//减去列表头部条目,获得在内容条目中的序号位置
View[] viewArr = new View[count];
int index = 0;
for (int i = begin; i < end; i++) {
View view = getChildAt(i);
if (view.getVisibility() == View.INVISIBLE) {
viewArr[position] = view;//不可见的子视图,就是当前拖动图标原来的条目视图,应该是唯一一个不可见
} else {//可见的子视图
if (index == position) {//跳过与当前在拖动的目标位置相同的位置,即让位,而此位置也即viewArr[position];在上一个语句已经记录了,就是拖动的原来条目视图,INVISIBLE
index++;
}
viewArr[index++] = view;
}
}
AnimTimeLine moveAnimTimeLine = new AnimTimeLine();
int toY = 0;
for (int i = 0; i < headViewCount; ++i) {
toY += getChildAt(i).getHeight();//计算第0个内容列表的图标,它的位置应该是去除列表头图标,比如那个切换应用的固定图标
}
//为需要挪动的图标加上动画
for (int i = 0; i < viewArr.length; i++) {
View view = viewArr[i];
int fromY = (int) view.getY();
//图标的原位置与目标位置不同,就加动画,去挪到目标位置,viewArr包括了不可见的拖动原图标视图,
//这样就是已经导致列表图标位置发生改变了
if (fromY != toY) {
Anim anim = new Anim(view, Anim.TRANSLATE, 200, Anim.CUBIC_OUT, from, to);
moveAnimTimeLine.addAnim(anim);
}
toY += view.getHeight();//从第0个开始,不断加上图标间隔
}
//动画开始,动画结束,图标的位置保持结束时的位置,即完成了图标调整
moveAnimTimeLine.start();
}
}
垃圾桶Trash对拖动触摸事件ACTION_MOVE的处理:
class Trash{
public void dragObjectMoveTo(float x, float y) {
if (inTrashReactArea(x, y)) {
//in trash area
trashFloatUpWithAnim(null); //垃圾桶冒出来,动画效果
} else {
//out trash area
trashFallDownWithAnim();//垃圾桶收回去,动画效果
}
}
}
触摸事件派发,手指松开后处理
public class SidebarRootView extends FrameLayout {
private void precessTouch(MotionEvent event) {
... ...
switch (action) {
//触摸放开,处理拖动结果,两种情况
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP : {
if (ENABLE_TOUCH_LOG) log.error("ACTION_UP");
//情况二:放进垃圾桶
if (mTrash.dragObjectUpOnUp(x, y, mDragView)) {
//handle uninstall
} else {//情况一,侧边栏放下,重新排序图标
dropDrag();
mTrash.trashDisappearWithAnim(null);//垃圾桶,动画退出
}
if (mDragView != null) {//让DragScrollView取消滚动处理
// we need action_update to stop scroll !
mSideView.dragObjectMove(event, eventTime);
}
break;
}
}
}
//情况一,侧边栏放下拖放图标的后续处理,先来个动画
public void dropDrag() {
... ...
mDragDroping = true;//标记拖动状态为放下图标,后续会用到
//calculate icon loc, bubble will hide, move icon to right loc
...
mDragView.mView.setTranslationX(iconLoc[0]);
mDragView.mView.setTranslationY(iconLoc[1]);
mDragView.hideBubble();
final ViewTreeObserver observer = mDragView.mView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
observer.removeOnGlobalLayoutListener(this);
//来个动画,拖动图标从手指的位置移动进去目标位置,且图标从大到小变化,因为拖动图标在拖动时是被放大了显示的
DropAnim anim = new DropAnim(mDragView);
anim.setDuration(200);
mDragView.mView.startAnimation(anim);
}
});
}
private class DropAnim extends Animation {
... ...
//动画结束,删除拖动伪图标,更新排序后数据到数据库, 处理图标列表位置更新,最后变正常显示
private void complete() {
mDragging = false;
mDragDroping = false;
//里面调用了内容列表视图处理位置更新,包括通知数据适配器,数据库更新
//这才是拖动排序的最终关键处理,真是历尽千辛万苦
mDragView.backToPostion();
mDragView.removeView(); //伪图标被删除
//侧边栏,取消全屏,恢复正常宽度
SidebarController.getInstance(mContext).updateDragWindow(false);
}
}
//DragView对拖动触摸事件ACTION_UP/CANCEL的处理,转发给视图列表去更新位置显示,但其实是先更新数据库,再辗转回来更新视图
class DragView{
//内容列表图标更新显示,交由原来通过setDraggedList注入进来的SidebarListView处理
public void backToPostion() {
mListViewItem.setVisibility(View.VISIBLE);//原来被拖动的原始图标变成可见
//原来开始图标长按时,已经有通过setDraggedList记下是那个SidebarListView实例了
mSideView.getDraggedListView().dropBackSidebarItem();
}
}
}
public class SidebarListView extends ListView {
//目标条目图标更新位置
public void dropBackSidebarItem() {
if (mDraggedItem != null) {
if (mAdapter != null) {
//适配器数据更新,也就是条目数据索引更新,后面才真正反映到列表视图上
mAdapter.moveItemPostion(mDraggedItem, mDragPosition - this.getHeaderViewsCount());
}
//拖动结束,侧边栏正常显示,善后一些变量
dragEnd();
}
}
public void onDragEnd() {
if (mAdapter != null) {
mAdapter.onDragEnd();
}
}
}
public class AppListAdapter extends SidebarAdapter {
//对列表条目数据进行重新排序,然后,通知视图列表更新,通知数据库管理类更新
@Override
public void moveItemPostion(Object object, int index) {
index --;
AppItem item = (AppItem)object;
...
mAppItems.remove(item);
mAppItems.add(index, item);
onOrderChange();
}
private void onOrderChange() {
for(int i = 0; i < mAppItems.size(); ++ i){
mAppItems.get(i).setIndex(mAppItems.size() - 1 - i);
}
//通知数据库更新
mManager.updateOrder();
}
}
public class AppManager extends DataManager {
public void updateOrder() {
synchronized (mAddedAppItems) {
Collections.sort(mAddedAppItems, new AppItem.IndexComparator());
}
notifyListener();//通知监听器,这是父类DataManager的方法,见下文
mHandler.obtainMessage(MSG_SAVE_ORDER).sendToTarget();//异步去更新数据库
}
private AppManager(Context context) {
//构造时就搞了个子线程在跑了,有消息来,就干活
HandlerThread thread = new HandlerThread(ResolveInfoManager.class.getName());
thread.start();
mHandler = new AppManagerHandler(thread.getLooper());
... ...
}
//消息来了,干活
private class AppManagerHandler extends Handler {
... ...
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SAVE_ORDER: //更新所有重新排序了的数据
mDatabase.saveOrderForList(getAddedAppItem());
... ...
}
}
}
}
public abstract class DataManager {
private List mListeners = new ArrayList();
//通知所有的RecentUpdateListener
protected void notifyListener(){
for(RecentUpdateListener lis : mListeners){
lis.onUpdate();
}
}
}
public class AppListAdapter extends SidebarAdapter {
//数据更新监听器
private DataManager.RecentUpdateListener resolveInfoUpdateListener = new DataManager.RecentUpdateListener() {
@Override
public void onUpdate() {
// do anim first !
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
//通知视图列表更新
mListView.animWhenDatasetChange();
}
});
}
};
}
public class SidebarListView extends ListView {
//适配数据更新调用过来,视图列表更新图标列表
public void animWhenDatasetChange() {
...
int time = 200;
mDatasetChangeTimeLine = new AnimTimeLine();
ListAdapter adapter = getAdapter();
if (adapter != null) {
for (int i = 0; i < adapter.getCount(); ++i) {
Object obj = adapter.getItem(i);
if (obj != null && obj instanceof SidebarItem) {
SidebarItem item = (SidebarItem) obj;
if (item.newAdded) {
...
mDatasetChangeTimeLine.addAnim(alphaAnim);
mDatasetChangeTimeLine.addAnim(scaleBigAnim);
mDatasetChangeTimeLine.addAnim(scaleNormal);
} else if(item.newRemoved) {
...
mDatasetChangeTimeLine.addAnim(alphaAnim);
mDatasetChangeTimeLine.addAnim(scaleBigAnim);
} else {
view.setAlpha(1.0f);
view.setScaleX(1.0f);
view.setScaleY(1.0f);
}
}
}
}
if (mListener != null) {
if (mDatasetChangeTimeLine.getAnimList() == null
|| mDatasetChangeTimeLine.getAnimList().size() == 0) {
mListener.onComplete(0);
} else {
mDatasetChangeTimeLine.setAnimListener(mListener);
}
}
//动画开始,动画结束会回调AnimListener mListener
mDatasetChangeTimeLine.start();
}
private AnimListener mListener = new AnimListener() {
@Override
public void onStart() {
}
@Override
public void onComplete(int type) {//动画结束
if(mAdapter != null) {
mAdapter.updateData();//更新数据
}
}
};
}
public class AppListAdapter extends SidebarAdapter {
//更新数据
public void updateData() {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
mAppItems = mManager.getAddedAppItem();//从数据库重新获取数据
notifyDataSetChanged();//通知视图列表更新
... ...
}
});
}
}
至此,拖动的伪图标被删除,侧边栏图标列表按拖动后的位置更新数据、并显示,侧边栏宽度恢复到正常宽度,垃圾桶归位,一切恢复正常。
情况二,放进垃圾桶的处理:
垃圾桶删除图标比较简单,关键是显示个对话框,根据用户选择,决定是否删除图标
class Trash{
//如果拖动到垃圾桶有效区域,则作删除图标的处理
public boolean dragObjectUpOnUp(float x, float y, DragView dragView) {
if (!inTrashUninstallReactArea(x, y)) {
return false;
}
//move icon to trash
moveIconToTrash(dragView);//来个动画,图标在垃圾桶上边悬浮摇动
mUninstallAction = new UninstallAction(mContext, dragView);
//显示删除图标的对话框,有意思的是,如果选择不删除,还会有个动画,把图标从垃圾桶丢回侧边栏
mUninstallAction.showUninstallDialog();
return true;
}
}