首先还是一贯作风,先看一些案例:(微信界面)
玩过微信的朋友想必很熟悉,其实就是界面可以拖拽,会有反弹效果,看起来很炫酷.(总之比拖拽没反应的死板要好.)
下面我来一一讲解如何实现这样效果:
原理:自定义ScrollView对其Touch监听,对布局时时更改.
一:不包含孩子,就是只针对普通的布局:
MyScrollView.java
- package com.rebound.myscroll;
- import android.content.Context;
- import android.graphics.Rect;
- import android.util.AttributeSet;
- import android.view.MotionEvent;
- import android.view.View;
- import android.view.animation.TranslateAnimation;
- import android.widget.ScrollView;
- /**
- * 自定义ScrollView
- */
- public class MyScrollView extends ScrollView {
- private View inner;// 孩子
- private float y;// 坐标
- private Rect normal = new Rect();// 矩形空白
- public MyScrollView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- /***
- * 根据 XML 生成视图工作完成.该函数在生成视图的最后调用,在所有子视图添加完之后. 即使子类覆盖了 onFinishInflate
- * 方法,也应该调用父类的方法,使该方法得以执行.
- */
- @Override
- protected void onFinishInflate() {
- if (getChildCount() > 0) {
- inner = getChildAt(0);// 获取其孩子
- }
- }
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- if (inner != null) {
- commOnTouchEvent(ev);
- }
- return super.onTouchEvent(ev);
- }
- /***
- * 触摸事件
- *
- * @param ev
- */
- public void commOnTouchEvent(MotionEvent ev) {
- int action = ev.getAction();
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- y = ev.getY();// 获取点击y坐标
- break;
- case MotionEvent.ACTION_UP:
- if (isNeedAnimation()) {
- animation();
- }
- break;
- case MotionEvent.ACTION_MOVE:
- final float preY = y;
- float nowY = ev.getY();
- int deltaY = (int) (preY - nowY);// 获取滑动距离
- y = nowY;
- // 当滚动到最上或者最下时就不会再滚动,这时移动布局
- if (isNeedMove()) {
- if (normal.isEmpty()) {
- // 填充矩形,目的:就是告诉this:我现在已经有了,你松开的时候记得要执行回归动画.
- normal.set(inner.getLeft(), inner.getTop(),
- inner.getRight(), inner.getBottom());
- }
- // 移动布局
- inner.layout(inner.getLeft(), inner.getTop() - deltaY / 2,
- inner.getRight(), inner.getBottom() - deltaY / 2);
- }
- break;
- default:
- break;
- }
- }
- /***
- * 开启动画移动
- */
- public void animation() {
- // 开启移动动画
- TranslateAnimation ta = new TranslateAnimation(0, 0, inner.getTop(),
- normal.top);
- ta.setDuration(300);
- inner.startAnimation(ta);
- // 设置回到正常的布局位置
- inner.layout(normal.left, normal.top, normal.right, normal.bottom);
- normal.setEmpty();// 清空矩形
- }
- /***
- * 是否需要开启动画
- *
- * 如果矩形不为空,返回true,否则返回false.
- *
- *
- * @return
- */
- public boolean isNeedAnimation() {
- return !normal.isEmpty();
- }
- /***
- * 是否需要移动布局 inner.getMeasuredHeight():获取的是控件的高度
- * getHeight():获取的是当前控件在屏幕中显示的高度
- *
- * @return
- */
- public boolean isNeedMove() {
- int offset = inner.getMeasuredHeight() - getHeight();
- int scrollY = getScrollY();
- // 0是顶部,后面那个是底部
- if (scrollY == 0 || scrollY == offset) {
- return true;
- }
- return false;
- }
- }
效果:
下拉效果,松开会自动回缩.
效果图是我随便弄的,这个比较简单,下面我们看下如果自定义的ScrollView里面包含ViewGroup类的控件如何办?
二:自定义ScrollView里面包含ListView.
想必这种效果比较常见,用处也比较广.但是单存的用上面自定义的ScrollView是行不通的,滑动时候相当不灵敏,发生错乱.为何:因为Touch受到影响,因为要继续向下传递嘛.
我们要对上面自定义稍做修改:
MyScrollView.java
- package com.jj.drag;
- import android.content.Context;
- import android.graphics.Rect;
- import android.util.AttributeSet;
- import android.util.Log;
- import android.view.MotionEvent;
- import android.view.View;
- import android.view.animation.TranslateAnimation;
- import android.widget.ScrollView;
- /**
- * ScrollView反弹效果的实现
- */
- public class MyScrollView extends ScrollView {
- private View inner;// 孩子View
- private float y;// 点击时y坐标
- private Rect normal = new Rect();// 矩形(这里只是个形式,只是用于判断是否需要动画.)
- private boolean isCount = false;// 是否开始计算
- public MyScrollView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- /***
- * 根据 XML 生成视图工作完成.该函数在生成视图的最后调用,在所有子视图添加完之后. 即使子类覆盖了 onFinishInflate
- * 方法,也应该调用父类的方法,使该方法得以执行.
- */
- @Override
- protected void onFinishInflate() {
- if (getChildCount() > 0) {
- inner = getChildAt(0);
- }
- }
- /***
- * 监听touch
- */
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- if (inner != null) {
- commOnTouchEvent(ev);
- }
- return super.onTouchEvent(ev);
- }
- /***
- * 触摸事件
- *
- * @param ev
- */
- public void commOnTouchEvent(MotionEvent ev) {
- int action = ev.getAction();
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- break;
- case MotionEvent.ACTION_UP:
- // 手指松开.
- if (isNeedAnimation()) {
- animation();
- isCount = false;
- }
- break;
- /***
- * 排除出第一次移动计算,因为第一次无法得知y坐标, 在MotionEvent.ACTION_DOWN中获取不到,
- * 因为此时是MyScrollView的touch事件传递到到了LIstView的孩子item上面.所以从第二次计算开始.
- * 然而我们也要进行初始化,就是第一次移动的时候让滑动距离归0. 之后记录准确了就正常执行.
- */
- case MotionEvent.ACTION_MOVE:
- final float preY = y;// 按下时的y坐标
- float nowY = ev.getY();// 时时y坐标
- int deltaY = (int) (preY - nowY);// 滑动距离
- if (!isCount) {
- deltaY = 0; // 在这里要归0.
- }
- y = nowY;
- // 当滚动到最上或者最下时就不会再滚动,这时移动布局
- if (isNeedMove()) {
- // 初始化头部矩形
- if (normal.isEmpty()) {
- // 保存正常的布局位置
- normal.set(inner.getLeft(), inner.getTop(),
- inner.getRight(), inner.getBottom());
- }
- Log.e("jj", "矩形:" + inner.getLeft() + "," + inner.getTop()
- + "," + inner.getRight() + "," + inner.getBottom());
- // 移动布局
- inner.layout(inner.getLeft(), inner.getTop() - deltaY / 2,
- inner.getRight(), inner.getBottom() - deltaY / 2);
- }
- isCount = true;
- break;
- default:
- break;
- }
- }
- /***
- * 回缩动画
- */
- public void animation() {
- // 开启移动动画
- TranslateAnimation ta = new TranslateAnimation(0, 0, inner.getTop(),
- normal.top);
- ta.setDuration(200);
- inner.startAnimation(ta);
- // 设置回到正常的布局位置
- inner.layout(normal.left, normal.top, normal.right, normal.bottom);
- Log.e("jj", "回归:" + normal.left + "," + normal.top + "," + normal.right
- + "," + normal.bottom);
- normal.setEmpty();
- }
- // 是否需要开启动画
- public boolean isNeedAnimation() {
- return !normal.isEmpty();
- }
- /***
- * 是否需要移动布局 inner.getMeasuredHeight():获取的是控件的总高度
- *
- * getHeight():获取的是屏幕的高度
- *
- * @return
- */
- public boolean isNeedMove() {
- int offset = inner.getMeasuredHeight() - getHeight();
- int scrollY = getScrollY();
- Log.e("jj", "scrolly=" + scrollY);
- // 0是顶部,后面那个是底部
- if (scrollY == 0 || scrollY == offset) {
- return true;
- }
- return false;
- }
- }
我们要排除出第一次ACTION_MOVE移动计算,因为第一次无法得知y坐标, 在MotionEvent.ACTION_DOWN中是获取不到, 因为此时是MyScrollView的touch事件传递到到了LIstView的孩子item上面.所以从第二次计算开始.(这里其实不应该这么搞,但是想不到更好的方法就只有这么搞了.如有好方法,记得share). 然而我们也要进行对第一次移动的距离deltaY进行初始化,就是第一次移动的时候让滑动距离归0. 之后的在进行记录执行相应的计算.
哈哈,明白了吧.其实也不难,可是也花了我好些时间.看下效果图吧.
效果图:
原样 下拉会自动缩回 上拉会自动缩回
看着勉强凑合吧,重要的是实现.
/********************************仔细品读想必对你有帮助*****************************************/
在这里我额外补充一点,不管你是否遇到过,总之掌握了对你没有害处,为之,我可是花费了2个小时才发现错误在哪里,原因:自己很土鳖.呵呵,不瞎扯了.
adapter.notifyDataSetChanged();这句话想必大家都不陌生, 作用:动态更新UI数据用的.
用法简单说明:异步或开线程对数据源BaseAdapter里面的数据进行更新.然后在主线程中执行adapter.notifyDataSetChanged();(其实就是从新执行getView方法,自己可以调试try 一下.)
但是我们这里用的不是单纯的ListView,而是自定义的ScrollView包裹着ListView,而ListView和ScrollView本来是冤家,想必现在大家都明白怎么解决,就是我setAdapter();后再次对ListView布局从新布局,这样有效的解决了二者的冲突.
方法如下:(大家都看的明白,这里就不过多解释)
- /***
- * 动态设置listview的高度
- *
- * @param listView
- */
- public void setListViewHeightBasedOnChildren(ListView listView) {
- ListAdapter listAdapter = listView.getAdapter();
- if (listAdapter == null) {
- return;
- }
- int totalHeight = 0;
- for (int i = 0; i < listAdapter.getCount(); i++) {
- View listItem = listAdapter.getView(i, null, listView);
- listItem.measure(0, 0);
- totalHeight += listItem.getMeasuredHeight();
- }
- ViewGroup.LayoutParams params = listView.getLayoutParams();
- params.height = totalHeight
- + (listView.getDividerHeight() * (listAdapter.getCount() - 1))
- + 15;
- listView.setLayoutParams(params);
- }
牛逼人物我想一目了然就知道哪里,但是我却花了2个小时,原因:还是原理没搞懂.
解释:我们首先解释下显示的效果原理:首先我们先把ListView show出来了,因为和ScrollView冲突然后我们又调用setListViewHeightBasedOnChildren();进行重新排版布局,才使得显示我们要的那种效果,之后我们对数据源做的修改,调用了adapter.notifyDataSetChanged();原理上应该显示出来了(此时数据源已经更新),因为此时我们setListViewHeightBasedOnChildren();只是显示我们修改数据之前,所以UI是不会更新的,所以要想更新,那么我们得从新调用一下setListViewHeightBasedOnChildren();根据最新的数据源绘图,这样就不会出错了。大家明白了吧,只要明白道理了,其实很简单.
效果图就不展示了,想必大家都知道是what.
上诉有一个小BUG,就是如果当数据超出一屏的话,你下拉的时候不松开,然后慢慢向上移动,你会发现跑得那是相当的快,如果想做成微信那种效果就是下拉的时候刷新,那么相当不好控制,原因其实很简单:当我们下拉的时候用的根本不是ScrollView的滑动.我们是通过对layout布局进行时时更新,此时的getScrollY()始终为0,但是你停止下拉的时候反而向上滑动的时候这个时候getScrollY()就不为0,原因为什么呢,因为我们下拉其实就是让布局向下移动了,抛开这里其实就相当于我滑动了,所以你上滑动的时候会ScrollView滑动会影响你,(自己可以测试看下log.)这就是bug原因,解决办法:我们只需要在滑动中添加: scrollBy(0, -deltaY);当超过一屏的时候ScrollView就会有自己的滑动,但是必须取消该滑动,因为我们有自己的滑动,作用:抵消ScrollView自带滑动.从而至运用对布局时时更新.
问题解决了,不过又有一个扯淡的问题,我到底在瞎忙什么,直接用简单的布局文件不就行了,为何要用ScrollView呢,还有ScrollView这个东西和ListView冲突在加上对Touch事件的分发处理不是那么协调,NND,搞了一下午,现在头都大了.
就说到这里,如有疑问请留言。
另外,如果对您有帮助的话,记得赞一个哦.
在此:Thanks for you !