前言

众所周知,与android相比,IOS的界面更炫,用户体验更好。现在Android虽然已经到了4.4,在UI上已经得到了很大的提升,但是依然和IOS有一定的差距。例如,IOS上的控件在拖拽时会有弹性效果,在拖拽控件时,控件会随手势的移动而移动,并且停止拖拽放开手指时。本人没有开发过IOS程序,对IOS的了解不是很深,但是从搞IOS开发的同事那里得知,控件会回弹这种效果是IOS默认的。

Android上的控件没有这种效果,这就导致用户在使用Android应用时,会感觉比较生硬,交互性不是很好。本文会提供一种解决方案,实现类似IOS的弹性效果。Android中的控件有很多,在这些控件中最常用的一个组件就是ListView,原生的ListView用户体验并不好,没有自带下拉刷新等功能,对Item排序, 左右滑动Item显示选项等功能也没有加入。(github上有很多关于ListView的开源项目,实现了这些功能)本文实现了ListView在滑到第一个条目时,继续下拉时的弹性效果。

首先介绍一下实现的基本原理。主要的实现机制是为ListView添加一个headerView, 该headerView的原始高度为0,监听触摸事件,根据下拉的距离动态改变headerView的高度,并且让headerView及时重绘,在放开手指时,重新设置headerView的高度为0,这样的话listView就会回弹到原始状态。如下图所示:



实现

下面以代码的形式介绍实现机制:

1 首先创建一个PullListView, 继承自SDK中的ListView类,在构造方法中创建一个HeaderView,设置HeaderView的高度为0,并且调用addHeaderView方法添加HeaderView。代码如下所示:

/** * 构造函数 * @param context */public PullListView(Context context) {super(context);setOnScrollListener(this);//创建PullListView的headerviewheadView = new View(this.getContext());headView.setBackgroundColor(Color.WHITE);headView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.FILL_PARENT, 0));this.addHeaderView(headView);}

2 覆盖父类ListView的onTouchEvent方法, 监听下拉手势。

在ACTION_DOWN事件中判断是否已经滑动到顶部。如果滑动到顶部,则记录下来手势的起点状态,如果在按下时,没有滑动到顶部,也就是第一个Item不可见,那么就不记录这个状态,还是让listview执行它默认的行为。代码如下所示:

switch (event.getAction()) {case MotionEvent.ACTION_DOWN:if (firstItemIndex == 0 ) {isRecored = true;startY = (int) event.getY();}break;

这里的firstItemIndex是成员变量,表示第一个可见的Item的索引是不是为0, 如果为0就表示已经滑动到顶部,再继续下拉时就可以显示弹性效果。 PullListView实现了OnScrollListener接口, 在构造方法中设置本身的OnScrollListener,监听滚动事件,并且根据滚动事件改变firstItemIndex的值。代码如下:

public class PullListView extends ListView implements OnScrollListener{

public PullListView(Context context) {super(context);setOnScrollListener(this);

public void onScroll(AbsListView view, int firstVisiableItem,int visibleItemCount, int totalItemCount) {firstItemIndex = firstVisiableItem;}public void onScrollStateChanged(AbsListView view, int scrollState) {currentScrollState = scrollState;}

在ACTION_MOVE事件中,监听下拉手势。并且只有记录下了起点状态才能执行相关逻辑,如果没有记录下起点状态,那么再次监听随着移动,是否滑动到顶点,如果在MOVE事件的过程中,ListView滑动到了第一个条目,那么同样记录下起点状态,如果再继续下滑,就可以执行弹性效果的相关逻辑。 执行弹性效果的相关逻辑之前,还要判断是不是向下滑动,如果是向上滑动的,则不执行任何操作。在下滑的过程中计算滑动的距离,随着下滑距离的增加,改变headView的高度,并且请求重绘。相关代码如下:

case MotionEvent.ACTION_MOVE:if (!isRecored && firstItemIndex == 0 ) {isRecored = true;startY = (int) event.getY();}if(!isRecored){break;}int tempY = (int) event.getY();int moveY = tempY - startY;if(moveY < 0){break;}headView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.FILL_PARENT,  (int)(moveY * PULL_FACTOR)));headView.invalidate();break;}

PULL_FACTOR是一个因子,它被定义成一个float类型的常量,值为0.6。这个因子的作用是实现下拉时ListView跟随手指移动的延迟效果,例如向下滑动了100像素, 那么 ListView并不会向下移动100像素, 而是移动60像素。这样的话就有了一个延迟,在下拉时就感觉不那么生硬。

在ACTION_UP事件中监听手指离开屏幕的操作,在离开屏幕时, 设置headView的高度为0,并且请求重绘, 那么ListView就回弹到初始状态。优化之前的代码如下所示:

case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:if(!isRecored){break;}headView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.FILL_PARENT, 0));headView.invalidate();isRecored = false;break;

这样的话能实现回弹效果, 但是headView的高度从一个较大的值瞬间变成0,同样让用户感觉生硬,容易闪瞎用户的眼 :) 。那么怎么解决这个问题呢?我们知道ACTION_MOVE事件时按一定的频率触发的,所以在 ACTION_MOVE中能够多次改变headview的高度,并且它的高度是逐渐增加的,这样就有平滑的效果,能够让ListView跟随用户手指的移动而移动。但是ACTION_UP事件在整个手势期间只会触发一次,所以无法达到渐变的效果。那么我们只能模拟这种渐变效果。在这里, 我使用的是Java 5线程并发库中的可调度线程池(ScheduledExecutorService)。该类能够按一定的频率重复多次执行一个任务。在按一定的频率执行任务时, 每次都会使用一个预定义的Handler对象发送消息,并且在处理消息时,递减headview的高度并重绘,等到headview的高度递减到0时,就停掉这个周期性的任务。相关代码如下:

private Handler handler = new Handler(){@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);AbsListView.LayoutParams params = (LayoutParams) headView.getLayoutParams();params.height -= PULL_BACK_REDUCE_STEP;headView.setLayoutParams(params);headView.invalidate();if(params.height <= 0){schedulor.shutdownNow();}}};

case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:if(!isRecored){break;}//headView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.FILL_PARENT, 0));//headView.invalidate();schedulor = Executors.newScheduledThreadPool(1);schedulor.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {handler.obtainMessage().sendToTarget();Log.i("testFixedRate", "xxxxxxxxxx");}}, 0, PULL_BACK_TASK_PERIOD, TimeUnit.NANOSECONDS);isRecored = false;break;

这里定义了两个常量:PULL_BACK_REDUCE_STEP和PULL_BACK_TASK_PERIOD。 PULL_BACK_REDUCE_STEP表示headview高度每次递减的像素数,这里定义为1;PULL_BACK_TASK_PERIOD表示间隔多长时间递减一次headview的高度,这里定义为700,注意单位是纳秒。也就是说, 在回弹时,每间隔700纳秒递减一次headview的高度,每次递减1个像素。这两个值是经过测试而设定的,如果设置不恰当,会使回弹过快或过慢, 并且在回弹的过程中出现一跳一跳的卡顿现象。

这里为什么要用handler呢?因为任务是在子线程中调度的,而在子线程中不能操作view,也就是不能设置view的宽度,所以要用一个handler在主线程中处理。

上面就是该PullListView的所有实现。因为代码并不是很多,所以在下面给出所有代码。

全部代码

import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.TimeUnit;import android.content.Context;import android.graphics.Color;import android.os.Handler;import android.os.Message;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.widget.AbsListView;import android.widget.AbsListView.OnScrollListener;import android.widget.ListView;/** * 下拉时具有弹性的ListView * @author zhangjg * @date Dec 21, 2013 4:54:29 PM */public class PullListView extends ListView implements OnScrollListener{private static final String TAG = "PullListView";//下拉因子,实现下拉时的延迟效果private static final float PULL_FACTOR = 0.6F;//回弹时每次减少的高度private static final int PULL_BACK_REDUCE_STEP = 1;//回弹时递减headview高度的频率, 注意以纳秒为单位private static final int PULL_BACK_TASK_PERIOD = 700;//记录下拉的起始点private boolean isRecored;//记录刚开始下拉时的触摸位置的Y坐标private int startY; //第一个可见条目的索引private int firstItemIndex;//用于实现下拉弹性效果的headViewprivate View headView;private int currentScrollState;//实现回弹效果的调度器private ScheduledExecutorService  schedulor;//实现回弹效果的handler,用于递减headview的高度并请求重绘private Handler handler = new Handler(){@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);AbsListView.LayoutParams params = (LayoutParams) headView.getLayoutParams();//递减高度params.height -= PULL_BACK_REDUCE_STEP;headView.setLayoutParams(params);//重绘headView.invalidate();//停止回弹时递减headView高度的任务if(params.height <= 0){schedulor.shutdownNow();}}};/** * 构造函数 * @param context */public PullListView(Context context) {super(context);init();}/** * 构造函数 * @param context * @param attr */public PullListView(Context context, AttributeSet attr) {super(context, attr);init();}/** * 初始化 */private void init() {//监听滚动状态setOnScrollListener(this);//创建PullListView的headviewheadView = new View(this.getContext());//默认白色背景,可以改变颜色, 也可以设置背景图片headView.setBackgroundColor(Color.WHITE);  //默认高度为0headView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.FILL_PARENT, 0));this.addHeaderView(headView);}/** * 覆盖onTouchEvent方法,实现下拉回弹效果 */@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN://记录下拉起点状态if (firstItemIndex == 0 ) {isRecored = true;startY = (int) event.getY();}break;case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:if(!isRecored){break;}//以一定的频率递减headview的高度,实现平滑回弹schedulor = Executors.newScheduledThreadPool(1);schedulor.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {handler.obtainMessage().sendToTarget();}}, 0, PULL_BACK_TASK_PERIOD, TimeUnit.NANOSECONDS);isRecored = false;break;case MotionEvent.ACTION_MOVE:if (!isRecored && firstItemIndex == 0 ) {isRecored = true;startY = (int) event.getY();}if(!isRecored){break;}int tempY = (int) event.getY();int moveY = tempY - startY;if(moveY < 0){isRecored = false;break;}headView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.FILL_PARENT,  (int)(moveY * PULL_FACTOR)));headView.invalidate();break;}return super.onTouchEvent(event);}public void onScroll(AbsListView view, int firstVisiableItem,int visibleItemCount, int totalItemCount) {firstItemIndex = firstVisiableItem;}public void onScrollStateChanged(AbsListView view, int scrollState) {currentScrollState = scrollState;}}


更多相关文章

  1. 第1个Android应用程序 Android制作简单单页导航
  2. Android进程永生技术终极揭秘:进程被杀底层原理、APP应对技巧
  3. android图片特效处理之光晕效果
  4. 在android 4.0 上面移植camera的一些心得 包括 单双camera 型号
  5. 在 Android(安卓)上使用 XML 和 JSON,第 2 部分: 交付混合了 JSON
  6. 我们把 iOS 的 Cocoa Touch 移植到了 Android
  7. Android如果对APK进行加密,提高反编译难度(思路)
  8. Android(安卓)小应用之一个activity实现简易手电筒(内附免费源码)
  9. Android(安卓)UI编程之自定义控件初步(上)——ImageButton

随机推荐

  1. Vue.js 中的无渲染行为插槽[每日前端夜话
  2. 关于 Promise 的 9 个面试题[每日前端夜
  3. 超好用的ai文章生成器 智媒ai伪原创平台
  4. Java实现定时任务的三种方法
  5. [Java] SpringMVC工作原理之一:Dispatcher
  6. 我的毕业季,没有一点点仪式感!
  7. 连夜再整理几个开源项目:练手/毕设/私活都
  8. 序列化系列(1)java序列化机制
  9. 废柴电脑拯救计划:搭个云服务器它不香嘛?
  10. 数据结构与算法(1)基本概念