2012年2月10日星期五

模拟三星通讯录滑动触发开关

三星手机的通讯录中的滑动开关非常方便, 左右滑动即可打电话或者发短信, 而无需点开联系人的详细资料;
   

今天就利用自定义ListView来模拟实现这个功能; 基本思路如下: 基本思路:
  1. 判断动作是否为横向滑动, 是则拦截并执行自己的方法, 不是则按默认执行;
  2. 绑定滑动监听器
  3. 绘制滑动时的效果
先看下模拟的效果:

 

下面就一步一步来实现它:

 1. 创建一个名为TouchListView 的类, 继承自 ListView

public class TouchListView extends ListView { 
    // 判断开关向左还是向右的条件 
    private final float leftOffset = 1f / 3f
    private final float rightOffset = 2f / 3f
    // 判断动作是否为横向滑动的条件 
    private final float xRange = 20f
    private final float yRange = 100f;
    // 当前状态: 是否为滑动 
    private boolean isSliding = false;
    // 手指按下时的位置 
    private float sx, sy
    // 手指移动过程中的位置 
    private float mx, my
    // 手指抬起时的位置 
    private float fx;
    // 滑动动作的目标Item 
    private View touchedView
    // 目标Item在列表中的位置 
    private int position
    // 目标Item在数据库中的id 
    private long id;
    // 滑动开关监听器 
    private TriggerListener tl
    // 绘制文字和彩带用的画笔 
    private Paint textPaint; private Paint mPaint
}

还有个init()方法在构造函数里调用, 用来初始化画笔;
private void init() {
    // 初始化文字画笔 
    textPaint = new Paint();
    textPaint.setColor(Color.WHITE); 
    textPaint.setTextSize(25);
    textPaint.setTextAlign(Align.CENTER); 
    textPaint.setAntiAlias(true);
    // 初始化彩带画笔    
    mPaint = new Paint(); 
    mPaint.setAntiAlias(true); 
}

2. 判断动作是否为横向滑动, 并加以拦截: 
因为和触摸事件有关, 所以我们需要重写 onTouchEvent() 方法;
@Override
public boolean onTouchEvent(MotionEvent ev) {
    // 当手指在屏幕上移动时即时记录手指的位置, 下面很多地方都要用到
    if(ev.getAction() == MotionEvent.ACTION_MOVE) {
        mx = ev.getX();
        my = ev.getY();
    }
    if(isSliding) { // 若状态为滑动则拦截, 并执行滑动时的动作
        sliding(ev);
        // 在传递前将动作设置为"取消"即可实现拦截
        ev.setAction(MotionEvent.ACTION_CANCEL);
    } else {
        // 非滑动状态时检查是否开始滑动
        checkSlide(ev);
    }
    return super.onTouchEvent(ev);
}

3. 判断是否为滑动的方法 checkSlide();
private void checkSlide(MotionEvent ev) { 
    int action = ev.getAction(); 
    switch(action) { 
    case MotionEvent.ACTION_DOWN: 
        // 记录开始位置, 即手指按下的位置 
        sx = ev.getX(); 
        sy = ev.getY(); 
        break
    case MotionEvent.ACTION_MOVE: 
        // 计算当前位移 
        float dx = Math.abs(mx - sx); 
        float dy = Math.abs(my - sy); 
        if(dx > xRange && dy < yRange) {  // 若横向位移大于 xRange, 纵向位移小于 yRange 则算作开始横向滑动 
            // 获得手指按下处的 Item 
            position = pointToPosition((int)sx, (int)sy); 
            int firstPosition = this.getFirstVisiblePosition(); 
            touchedView = getChildAt(position - firstPosition); 
            if(touchedView != null) {   // 只有Item存在时才算作开始滑动, 若在空白处滑动则不算 
                Log.i("Notebook", "开始横向滑动"); 
                // 将滑动状态设置为 true 
                this.isSliding = true
                // 获得 Item 的 id, 设置监听器时要用 
                id = getAdapter().getItemId(position); 
                // ListView 默认是只在上下滚动时才刷新, 所以这里要强制刷新它     
                this.postInvalidate(); 
            } 
        } 
        break
    } 
}

4. 进入滑动状态后要执行的动作 sliding():
private void sliding(MotionEvent ev) { 
    if(ev.getAction() == MotionEvent.ACTION_UP) { // 滑动时, 手指抬起代表滑动结束 
        // 首先, 重置滑动状态为 false 
        this.isSliding = false
        Log.i("Notebook", "滑动结束"); 
        // 记录手指抬起的位置 this.fx = ev.getX(); 
        // 抬起后判断开关的方向, 向左, 向右, 或是空 
        int width = touchedView.getWidth(); 
        int direction = TriggerListener.NONE
        if(fx > width * rightOffset) direction = TriggerListener.RIGHT;     
        else if(fx < width * leftOffset) direction = TriggerListener.LEFT
        // 确定方向后执行监听器定义的方法 
        if(tl != null) { 
            tl.onTrigger(direction, position, id); 
        }
    }
}

5. 设置动画效果(向左滑变红, 向右滑变红, 同时透明度改变); 需要重写 drawChild(); 方法
@Override 
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 
    boolean b = super.drawChild(canvas, child, drawingTime);
    // 画在原来的图像上方 
    if(isSliding) {        // 滑动时则开始绘制 // 若不是目标对象则按默认动作返回 
        if(touchedView != childreturn b; 
        Log.i("Notebook", "开始绘图"); 
        // 得到绘图所需基本数据 
        int top = touchedView.getTop(); 
        int width = touchedView.getWidth(); 
        int height = touchedView.getHeight(); 
        // 生成彩带 
        LinearGradient shader = new LinearGradient(mx - width / 2, 0, mx + width / 2, 0, Color.rgb(11,115,6), Color.RED, Shader.TileMode.CLAMP); 
        mPaint.setShader(shader); 
        // 计算手指偏移量(相对于中心) 
        float offset = Math.abs(mx - width / 2f) / (width / 2f); 
        // 设置透明度 
        textPaint.setAlpha((int) (255 * offset)); 
        mPaint.setAlpha((int) (200 + 55 * offset)); 
        // 移动画布前保存 
        canvas.save(); 
        // 将画布移动到要画的条目处 
        canvas.translate(0, top); 
        // 画彩带; 
        canvas.drawRect(0, 0, width, height, mPaint);
        // 画文字 
        String text = ""
        if(mx > width * rightOffset) text = "编辑"
        else if(mx < width * leftOffset) text = "删除"
        canvas.drawText(text, width / 2f, height / 2f, textPaint); 
        // 还原画布 
        canvas.restore(); 
        // 刷新View 
        this.postInvalidate(); 
    } 
    return b;
}

6. 绑定监听器的方法
/**
 * 绑定滑动监听器到 ListView
 * @param tl 滑动监听器
 */
public void setTriggerListener(TriggerListener tl) {
    this.tl = tl;
}

7. 监听器接口
public interface TriggerListener { 
    public static final int NONE = -1
    public static final int LEFT = 0
    public static final int RIGHT = 1
    /**      
     * 滑动操作的监听器     
     * @param direction 滑动操作的方向, 左, 右, 无     
     * @param Position 该条目在List中的位置     
     * @param id 该条目在数据库中的id     
     */ 
    public void onTrigger(int direction, int Position, long id); 
}

使用时和普通的ListView没什么区别, 只要记得实现TriggerListener接口绑定监听器就行了;