最近好多小伙伴都结婚了,甚至还有二婚的,我终于总结出为什么如此热爱生活的同时,钱包却一直不见涨的原因。想想以后的日子,结婚的生孩子,二婚的生孩子,然后有的结婚,有的二婚,有的多婚。。深深感叹:时不我待啊!!!
高中老师的谆谆教诲,一直牢记在心中,那时他说:兔子不是窝边草,你看看你们,都在自己班级处对象!那时看他痛心疾首,一副恨铁不成钢的样子,使我深有感触,一直坚持到现在。最近受伙伴们的刺激,我终于发觉,老师的话是错的,现在也只有窝边有草了。。。。于是乎,我冒出个想法,写完此篇博客,我要逛遍公司六层。不要问我为什么,这战乱的年代,我看看窝边还有没有能吃的草。。。
人们都说有压力才有动力,而我认为有动力未必有灵感,所以保持心情愉悦,灵感自来。
吹牛结束,开始正文:
如题,在我接触变成的时候,第一位导师常常挂在嘴边的话就是:思想最重要。对此我郁闷了很久很久。终于在日积月累的代码生涯中,领悟了这句话。至于什么时候会有思想,这点不好说,也许吃饭的时候,也许在卫生间思考的时候,也许做梦的时候。。
先看效果,由于模拟器问题,List的下划线显示的有些乱。真机上是没问题的
开篇之前,需要知道几个有关系的方法,当知道这个方法后,相信很多人就能够自己去实现了。
/**
* Maps a point to a position in the list.
*
* @param x X in local coordinate
* @param y Y in local coordinate
* @return The position of the item which contains the specified point, or
* {@link #INVALID_POSITION} if the point does not intersect an item.
*/
public int pointToPosition(int x, int y)
此方法的官方解释:映射一个点到列表的位置。
就是说:参数是手指按在屏幕的坐标,返回值是这个坐标所存在的item的position值。
既然想拖拽item,就必须要获取到这个item的对象。就需要用到下面的方法:
/**
* Returns the view at the specified position in the group.
*
* @param index the position at which to get the view from
* @return the view at the specified position or null if the position
* does not exist within the group
*/
public View getChildAt(int index)
此方法是根据获得的position值来获取对应position的item的View对象。这里需要注意一点,在我们日常的ListView开发中,经常需要对View对象进行复用优化。那么着就引起了接下来的问题,对象被复用后,在传入position这个位置值时,那么我们获取的对象就有可能是之前复用的。换句话说,我们的ListView优化后,屏幕上显示多少个item,也就有多少个View对象,如果有10个View对象的话,当我点击第18个item的时候,通过此方法,返回的对象实际上就是第八个item的实例,那么如何解决这个问题,Google提供了下面这个方法:
/**
* Returns the position within the adapter's data set for the first item
* displayed on screen.
*
* @return The position within the adapter's data set
*/
public int getFirstVisiblePosition()
这里返回的值,是当前屏幕上显示的所有item中得第一个item的position值。所以我们的用法是:
getChildAt(position - getFirstVisiblePosition())
这样我们得到的对象就是当前点击的item正在使用的View对象(如果复用,就是复用后的)。
当我们拖动item的时候,为了知道此时是正在拖动的状态和拖动的item是哪个,就需要让这个item的整体布局跟着手指来移动。那么我们获取到item的布局,然后让他跟着手指移动?很抱歉,布局已经绘制了,状态是locked。重新new一个布局来跟着手指拖动?先不说这样很麻烦,对于应用中得各种各样的item布局,难道还要绘制各种各样的View来对应?万能的谷姐同样提供了这样的方法:
/**
* <p>Enables or disables the drawing cache. When the drawing cache is enabled, the next call
* to {@link #getDrawingCache()} or {@link #buildDrawingCache()} will draw the view in a
* bitmap. Calling {@link #draw(android.graphics.Canvas)} will not draw from the cache when
* the cache is enabled. To benefit from the cache, you must request the drawing cache by
* calling {@link #getDrawingCache()} and draw it on screen if the returned bitmap is not
* null.</p>
*
* <p>Enabling the drawing cache is similar to
* {@link #setLayerType(int, android.graphics.Paint) setting a layer} when hardware
* acceleration is turned off. When hardware acceleration is turned on, enabling the
* drawing cache has no effect on rendering because the system uses a different mechanism
* for acceleration which ignores the flag. If you want to use a Bitmap for the view, even
* when hardware acceleration is enabled, see {@link #setLayerType(int, android.graphics.Paint)}
* for information on how to enable software and hardware layers.</p>
*
* <p>This API can be used to manually generate
* a bitmap copy of this view, by setting the flag to <code>true</code> and calling
* {@link #getDrawingCache()}.</p>
*
* @param enabled true to enable the drawing cache, false otherwise
*
* @see #isDrawingCacheEnabled()
* @see #getDrawingCache()
* @see #buildDrawingCache()
* @see #setLayerType(int, android.graphics.Paint)
*/
public void setDrawingCacheEnabled(boolean enabled)
一看这么一大段注释就头疼,其实就是说,将View的绘制进行缓存。缓存成什么东西呢?
/**
* <p>Calling this method is equivalent to calling <code>getDrawingCache(false)</code>.</p>
*
* @return A non-scaled bitmap representing this view or null if cache is disabled.
*
* @see #getDrawingCache(boolean)
*/
public Bitmap getDrawingCache()
缓存成Bitmap,恍然大明白,缓存成图像啊,那这回简单了,拖动哪个item,将它变成皂片就可以了。
OK,了解了以上的方法,再了解自定义View的常用方法。那么只要肯下功夫,Anyone can do。如果看到这里还有Anyone 不 can do。那就接着往下看吧。
首先定义一个响亮而又文雅的名字,这样才显着有范。然后是构造方法,初始化常量,然后处理事件。处理事件的逻辑是,在按下一定时间后,显示出对应item的皂片,咱后根据手指拖动,实时跟新皂片的位置,并进行item的更新,以达到替换效果。然后在停止拖动的时候释放资源。逻辑理清后,看看代码实现,注释都已经写在里面了:
package com.qiyuan.activity.view;
import android.R.integer;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;
public class CanDragListView extends ListView {
private ListAdapter mAdapter;
private WindowManager mWindowManager;
/**
* item镜像的布局参数
*/
private WindowManager.LayoutParams mWindowLayoutParams;
private WindowManager.LayoutParams mNewWindowLayoutParams;
/**
* 振动器
*/
private Vibrator mVibrator;
/**
* 选中的item的position
*/
private int mSelectedPosition;
/**
* 选中的item的View对象
*/
private View mItemView;
/**
* 用于拖拽的镜像,这里直接用一个ImageView装载Bitmap
*/
private ImageView mDragIV;
private ImageView mNewDragIv;
/**
* 选中的item的镜像Bitmap
*/
private Bitmap mBitmap;
/**
* 按下的点到所在item的上边缘的距离
*/
private int mPoint2ItemTop;
/**
* 按下的点到所在item的左边缘的距离
*/
private int mPoint2ItemLeft;
/**
* CanDragListView距离屏幕顶部的偏移量
*/
private int mOffset2Top;
/**
* CanDragListView自动向下滚动的边界值
*/
private int mDownScrollBorder;
/**
* CanDragListView自动向上滚动的边界值
*/
private int mUpScrollBorder;
/**
* CanDragListView自动滚动的速度
*/
private static final int speed = 20;
/**
* CanDragListView距离屏幕左边的偏移量
*/
private int mOffset2Left;
/**
* 状态栏的高度
*/
private int mStatusHeight;
/**
* 按下的系统时间
*/
private long mActionDownTime = 0;
/**
* 移动的系统时间
*/
private long mActionMoveTime = 0;
/**
* 默认长按事件时间是1000毫秒
*/
private long mLongClickTime = 1000;
/**
* 是否可拖拽,默认为false
*/
private boolean isDrag = false;
/**
* 按下是的x坐标
*/
private int mDownX;
/**
* 按下是的y坐标
*/
private int mDownY;
/**
* item发生变化回调的接口
*/
private OnChanageListener onChanageListener;
/**
* 设置回调接口
*
* @param onChanageListener
*/
public void setOnChangeListener(OnChanageListener onChanageListener) {
this.onChanageListener = onChanageListener;
}
public CanDragListView(Context context) {
this(context, null);
}
public CanDragListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CanDragListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mAdapter = getAdapter();
mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mStatusHeight = getStatusHeight(context); // 获取状态栏的高度
}
private Handler mHandler = new Handler();
// 用来处理长按的Runnable
private Runnable mLongClickRunnable = new Runnable() {
@Override
public void run() {
isDrag = true; // 设置可以拖拽
mVibrator.vibrate(100); // 震动100毫秒
if (mItemView != null) {
mItemView.setVisibility(View.INVISIBLE);// 隐藏该item
}
Log.i("CanDragListView", "**mLongClickRunnable**");
// 根据我们按下的点显示item镜像
createDragImage(mBitmap, mDownX, mDownY);
}
};
/**
* 当mDownY的值大于向上滚动的边界值,触发自动向上滚动 当mDownY的值小于向下滚动的边界值,触犯自动向下滚动 否则不进行滚动
*/
private Runnable mScrollRunnable = new Runnable() {
@Override
public void run() {
int scrollY;
if (mDownY > mUpScrollBorder) {
scrollY = speed;
mHandler.postDelayed(mScrollRunnable, 25);
} else if (mDownY < mDownScrollBorder) {
scrollY = -speed;
mHandler.postDelayed(mScrollRunnable, 25);
} else {
scrollY = 0;
mHandler.removeCallbacks(mScrollRunnable);
}
// 所以我们在这里调用下onSwapItem()方法来交换item
onSwapItem(mDownY, mDownY);
smoothScrollBy(scrollY, 10);
}
};
@Override
public boolean onTouchEvent(MotionEvent event) {
// Log.i("CanDragListView", mSelectedPosition+"****"+mItemView);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mActionDownTime = event.getDownTime();
mDownX = (int) event.getX();
mDownY = (int) event.getY();
// 根据按下的坐标获取item对应的position
mSelectedPosition = pointToPosition(mDownX, mDownY);
// 如果是无效的position,即值为-1
if (mSelectedPosition == AdapterView.INVALID_POSITION) {
return super.onTouchEvent(event);
}
// 根据position获取对应的item
mItemView = getChildAt(mSelectedPosition - getFirstVisiblePosition());
// 使用Handler延迟mLongClickTime执行mLongClickRunnable
mHandler.postDelayed(mLongClickRunnable, mLongClickTime);
if (mItemView != null) {
// 下面这几个距离大家可以参考我的博客上面的图来理解下
mPoint2ItemTop = mDownY - mItemView.getTop();
mPoint2ItemLeft = mDownX - mItemView.getLeft();
mOffset2Top = (int) (event.getRawY() - mDownY);
mOffset2Left = (int) (event.getRawX() - mDownX);
// 获取CanDragListView自动向上滚动的偏移量,小于这个值,CanDragListView向下滚动
mDownScrollBorder = getHeight() / 4;
// 获取CanDragListView自动向下滚动的偏移量,大于这个值,CanDragListView向上滚动
mUpScrollBorder = getHeight() * 3 / 4;
// 将该item进行绘图缓存
mItemView.setDrawingCacheEnabled(true);
// 从缓存中获取bitmap
mBitmap = Bitmap.createBitmap(mItemView.getDrawingCache());
// 释放绘图缓存,避免出现重复的缓存对象
mItemView.destroyDrawingCache();
}
// Log.i("CanDragListView", "****"+isDrag);
break;
case MotionEvent.ACTION_MOVE:
// TODO
if (isDrag) {
int moveX = (int) event.getX();
int moveY = (int) event.getY();
if (!isOnTouchInItem(mItemView, moveX, moveY)) {
mHandler.removeCallbacks(mLongClickRunnable);
}
mDownX = moveX;
mDownY = moveY;
onDragItem(moveX, moveY);
}
break;
case MotionEvent.ACTION_UP:
onStopDrag();
mHandler.removeCallbacks(mLongClickRunnable);
mHandler.removeCallbacks(mScrollRunnable);
isDrag = false;
break;
default:
break;
}
return super.onTouchEvent(event);
}
/**
* 判断手指按下的坐标是否在item范围内
*
* @param view
* @param downX
* @param downY
* @return
*/
private boolean isOnTouchInItem(View view, int downX, int downY) {
if (view == null) {
return false;
}
int leftX = view.getLeft();
int topY = view.getTop();
if (downX < leftX || downX > leftX + view.getWidth()) {
return false;
}
if (downY < topY || downY > topY + view.getHeight()) {
return false;
}
return true;
}
/**
* 创建拖动的镜像
*
* @param bitmap
* @param downX
* 按下的点相对父控件的X坐标
* @param downY
* 按下的点相对父控件的X坐标
*/
private void createDragImage(Bitmap bitmap, int downX, int downY) {
mWindowLayoutParams = new WindowManager.LayoutParams();
mWindowLayoutParams.format = PixelFormat.TRANSLUCENT; // 图片之外的其他地方透明
mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
mWindowLayoutParams.x = downX - mPoint2ItemLeft + mOffset2Left;
mWindowLayoutParams.y = downY - mPoint2ItemTop + mOffset2Top - mStatusHeight;
mWindowLayoutParams.alpha = 0.55f; // 透明度
mWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mNewWindowLayoutParams = new WindowManager.LayoutParams();
mNewWindowLayoutParams.format = PixelFormat.TRANSLUCENT; // 图片之外的其他地方透明
mNewWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
mNewWindowLayoutParams.x = downX - mPoint2ItemLeft + mOffset2Left;
mNewWindowLayoutParams.y = downY - mPoint2ItemTop + mOffset2Top - mStatusHeight;
// mNewWindowLayoutParams.alpha = 0.55f; // 透明度
mNewWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mNewWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mNewWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mDragIV = new ImageView(getContext());
mNewDragIv = new ImageView(getContext());
mDragIV.setImageBitmap(bitmap);
mWindowManager.addView(mDragIV, mWindowLayoutParams);
mWindowManager.addView(mNewDragIv, mNewWindowLayoutParams);
}
/**
* 移除镜像
*/
private void removeDragImage() {
if (mDragIV != null) {
mWindowManager.removeView(mDragIV);
mDragIV = null;
}
if (mNewDragIv != null) {
mWindowManager.removeView(mNewDragIv);
mNewDragIv = null;
}
}
/**
* 拖动item,在里面实现了item镜像的位置更新,item的相互交换以及ListView的自行滚动
*
* @param x
* @param y
*/
private void onDragItem(int moveX, int moveY) {
if (mWindowLayoutParams != null && mDragIV != null) {
mWindowLayoutParams.x = moveX - mPoint2ItemLeft + mOffset2Left;
mWindowLayoutParams.y = moveY - mPoint2ItemTop + mOffset2Top - mStatusHeight;
mWindowManager.updateViewLayout(mDragIV, mWindowLayoutParams); // 更新镜像的位置
}
onSwapItem(moveX, moveY);
// ListView自动滚动
mHandler.post(mScrollRunnable);
}
/**
* 交换item,并且控制item之间的显示与隐藏效果
*
* @param moveX
* @param moveY
*/
private void onSwapItem(int moveX, int moveY) {
// 获取我们手指移动到的那个item的position
int position = pointToPosition(moveX, moveY);
// 假如tempPosition 改变了并且tempPosition不等于-1,则进行交换
if (position != mSelectedPosition && position != AdapterView.INVALID_POSITION) {
// mAdapter.getItem(mSelectedPosition);
View newItem = getChildAt(position - getFirstVisiblePosition());
View oldItem = getChildAt(mSelectedPosition - getFirstVisiblePosition());
mNewWindowLayoutParams.x = moveX - (moveX - oldItem.getLeft()) + mOffset2Left;
mNewWindowLayoutParams.y = moveY - (moveY - oldItem.getTop()) + mOffset2Top - mStatusHeight;
newItem.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(newItem.getDrawingCache());
newItem.destroyDrawingCache();
mNewDragIv.setImageBitmap(bitmap);
if (newItem != null && oldItem != null) {
newItem.setVisibility(INVISIBLE);// 隐藏拖动到的位置的item
oldItem.setVisibility(VISIBLE);//显示之前的
mWindowManager.updateViewLayout(mNewDragIv, mNewWindowLayoutParams); // 更新镜像的位置
if (onChanageListener != null) {
Log.i("CanDragListView", "**onSwapItem**");
onChanageListener.onChange(mSelectedPosition, position);
}
}
mSelectedPosition = position;
}
}
/**
* 停止拖拽我们将之前隐藏的item显示出来,并将镜像移除
*/
private void onStopDrag() {
View view = getChildAt(mSelectedPosition - getFirstVisiblePosition());
if (view != null) {
view.setVisibility(View.VISIBLE);
}
// ((DragAdapter)this.getAdapter()).setItemHide(-1);
removeDragImage();
}
/**
* 获取状态栏的高度
*
* @param context
* @return
*/
private static int getStatusHeight(Context context) {
int statusHeight = 0;
Rect localRect = new Rect();
((Activity) context).getWindow().getDecorView().getWindowVisibleDisplayFrame(localRect);
statusHeight = localRect.top;
if (0 == statusHeight) {
Class<?> localClass;
try {
localClass = Class.forName("com.android.internal.R$dimen");
Object localObject = localClass.newInstance();
int i5 = Integer.parseInt(localClass.getField("status_bar_height").get(localObject).toString());
statusHeight = context.getResources().getDimensionPixelSize(i5);
} catch (Exception e) {
e.printStackTrace();
}
}
return statusHeight;
}
/**
* 监听数据拖拽的接口,用来更新数据显示
*/
public interface OnChanageListener {
/**
* 当item交换位置的时候回调的方法,在此处实现数据的交换
*
* @param start
* 开始的position
* @param to
* 拖拽到的position
*/
public void onChange(int start, int to);
}
}
在代码里定义了改变的回调接口,目的是对item进行交换并且更新。开始我是想将item的更新都固定在自定义的ListView中,这样拿来就能直接用。思考良久,无法完美的实现。如果谁有好的办法,记得教我一下。
对于item的更新,一定要在Activity中实现OnChanageListener后,在方法里对数据集合进行元素位置交换。这里用的方法是:
/**
* Swaps the elements of list {@code list} at indices {@code index1} and
* {@code index2}.
*
* @param list
* the list to manipulate.
* @param index1
* position of the first element to swap with the element in
* index2.
* @param index2
* position of the other element.
*
* @throws IndexOutOfBoundsException
* if index1 or index2 is out of range of this list.
* @since 1.4
*/
@SuppressWarnings("unchecked")
public static void swap(List<?> list, int index1, int index2)
参数是要交换数据的集合,和要交换的两个元素的位置。用法是:
mListView.setOnChangeListener(new OnChanageListener() {
@Override
public void onChange(int start, int to) {
//数据交换
if(start < to){
for(int i=start; i<to; i++){
Collections.swap(mList, i, i+1);
}
}else if(start > to){
for(int i=start; i>to; i--){
Collections.swap(mList, i, i-1);
}
}
mAdapter.notifyDataSetChanged();
}
});
到此,可拖拽的ListView已经完成。
布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/include_title" />
<com.qiyuan.activity.view.CanDragListView
android:id="@+id/main_activity_lv"
android:dividerHeight="0.2dp"
android:divider="@color/blueviolet"
android:footerDividersEnabled="false"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>