Android上实现仿IOS弹性ScrollView
来源:互联网 发布:淘宝技术这10年百度云 编辑:程序博客网 时间:2024/04/30 15:30
前言
IOS的UI和用户体验是它的优势, 与IOS相比, Android的UI和用户体验可能要差一些。 虽然Android版本已经到了4.4, 对系统的各个方法进行了大量实质性的优化, 但他的显示效果和交互体验依然不及IOS。 例如IOS上的很多控件都是带弹性的, 也就是拖拽一个控件到了该控件的边界, 但是控件依然可以随着手指的移动而移动一段距离。 这样的话, 给用户的感觉就不那么生硬,能在一定程度上提升用户体验。现在有很多Android App都采用了这中弹性控件,例如最常见的QQ, 多个界面都采用了弹性ScrollView。 我不知道腾讯是如何实现的, 在本文中我会给出自己的实现方式。
我之前的文章 Android上实现仿IOS弹性ListView , 在Android上实现了弹性的ListView, 主要的实现原理是为该ListView增加一个初始高度为0的HeaderView, 如果滚动到第一个条目的位置,用户依然向下拖拽, 那么就增加HeaderView的高度,实现拉伸效果, 当用户松开手指, 就将HeaderView的高度组件递减到0, 实现回弹效果。感兴趣的同学可以看一下实现细节, 文章的链接已经在上面给出。
弹性ScrollView的实现
本文讨论弹性ScrollView的实现。弹性ScrollView实现原理和ListView不同, 因为ScrollView只能有一个子View, 不能为他添加额外的HeaderView。 弹性ScrollView的实现原理是移动这个唯一的子View的布局。下面首先给出所有的实现代码, 再对实现中的几个关键点进行说明。 (实现原理比较简单, 代码逻辑也不是很复杂, 代码比较少)
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
* 实现下拉弹回和上拉弹回
* @author zhangjg
* @date Feb 13, 2014 6:11:33 PM
*/
public
class
ReboundScrollView
extends
ScrollView {
private
static
final
String TAG =
"ElasticScrollView"
;
//移动因子, 是一个百分比, 比如手指移动了100px, 那么View就只移动50px
//目的是达到一个延迟的效果
private
static
final
float
MOVE_FACTOR =
0
.5f;
//松开手指后, 界面回到正常位置需要的动画时间
private
static
final
int
ANIM_TIME =
300
;
//ScrollView的子View, 也是ScrollView的唯一一个子View
private
View contentView;
//手指按下时的Y值, 用于在移动时计算移动距离
//如果按下时不能上拉和下拉, 会在手指移动时更新为当前手指的Y值
private
float
startY;
//用于记录正常的布局位置
private
Rect originalRect =
new
Rect();
//手指按下时记录是否可以继续下拉
private
boolean
canPullDown =
false
;
//手指按下时记录是否可以继续上拉
private
boolean
canPullUp =
false
;
//在手指滑动的过程中记录是否移动了布局
private
boolean
isMoved =
false
;
public
ReboundScrollView(Context context) {
super
(context);
}
public
ReboundScrollView(Context context, AttributeSet attrs) {
super
(context, attrs);
}
@Override
protected
void
onFinishInflate() {
if
(getChildCount() >
0
) {
contentView = getChildAt(
0
);
}
}
@Override
protected
void
onLayout(
boolean
changed,
int
l,
int
t,
int
r,
int
b) {
super
.onLayout(changed, l, t, r, b);
if
(contentView ==
null
)
return
;
//ScrollView中的唯一子控件的位置信息, 这个位置信息在整个控件的生命周期中保持不变
originalRect.set(contentView.getLeft(), contentView.getTop(), contentView
.getRight(), contentView.getBottom());
}
/**
* 在触摸事件中, 处理上拉和下拉的逻辑
*/
@Override
public
boolean
dispatchTouchEvent(MotionEvent ev) {
if
(contentView ==
null
) {
return
super
.dispatchTouchEvent(ev);
}
int
action = ev.getAction();
switch
(action) {
case
MotionEvent.ACTION_DOWN:
//判断是否可以上拉和下拉
canPullDown = isCanPullDown();
canPullUp = isCanPullUp();
//记录按下时的Y值
startY = ev.getY();
break
;
case
MotionEvent.ACTION_UP:
if
(!isMoved)
break
;
//如果没有移动布局, 则跳过执行
// 开启动画
TranslateAnimation anim =
new
TranslateAnimation(
0
,
0
, contentView.getTop(),
originalRect.top);
anim.setDuration(ANIM_TIME);
contentView.startAnimation(anim);
// 设置回到正常的布局位置
contentView.layout(originalRect.left, originalRect.top,
originalRect.right, originalRect.bottom);
//将标志位设回false
canPullDown =
false
;
canPullUp =
false
;
isMoved =
false
;
break
;
case
MotionEvent.ACTION_MOVE:
//在移动的过程中, 既没有滚动到可以上拉的程度, 也没有滚动到可以下拉的程度
if
(!canPullDown && !canPullUp) {
startY = ev.getY();
canPullDown = isCanPullDown();
canPullUp = isCanPullUp();
break
;
}
//计算手指移动的距离
float
nowY = ev.getY();
int
deltaY = (
int
) (nowY - startY);
//是否应该移动布局
boolean
shouldMove =
(canPullDown && deltaY >
0
)
//可以下拉, 并且手指向下移动
|| (canPullUp && deltaY<
0
)
//可以上拉, 并且手指向上移动
|| (canPullUp && canPullDown);
//既可以上拉也可以下拉(这种情况出现在ScrollView包裹的控件比ScrollView还小)
if
(shouldMove){
//计算偏移量
int
offset = (
int
)(deltaY * MOVE_FACTOR);
//随着手指的移动而移动布局
contentView.layout(originalRect.left, originalRect.top + offset,
originalRect.right, originalRect.bottom + offset);
isMoved =
true
;
//记录移动了布局
}
break
;
default
:
break
;
}
return
super
.dispatchTouchEvent(ev);
}
/**
* 判断是否滚动到顶部
*/
private
boolean
isCanPullDown() {
return
getScrollY() ==
0
||
contentView.getHeight() < getHeight() + getScrollY();
}
/**
* 判断是否滚动到底部
*/
private
boolean
isCanPullUp() {
return
contentView.getHeight() <= getHeight() + getScrollY();
}
}
关键点解析
1 判断适合拉伸的时机
也就是说要判断什么时候开始拉伸, 当然是当ScrollView滑动到顶部或底部的时候。 是否移动到顶部或底部,需要根据三个值进行判断, 这三个值分别是ScrollView的高度, ScrollView中的子控件(在本例中是contentView变量)的高度, 和ScrollView在竖直方向上滚动的距离mScrollY。这三个数值的关系如下图所示:
其中位于下方的蓝色控件是ScrollView, 位于上方的是contentView。
所以mScrollY等于0的时候, 就说明ScrollView滚动到了顶部。 如下图所示:
当contentView.height() = scrollView.height + mScrollY时, 就说明滚动到了底部, 如下图所示:
还有一种情况既可以认为滚动到了底部,也可以认为滚动到顶部, 那就是contentView的高度本身就小于ScrollView的高度, 不需要滑动, 这时满足条件contentView.height() < scrollView.height + mScrollY。 如下图所示:
判断滚动到顶部和滚动到底部,分别由isCanPullDown方法和isCanPullUp方法实现。理解了上面图示的内容, 就可以很容易的理解这两个方法的原理。 这两个方法的代码如下所示:
/**
* 判断是否滚动到顶部
*/
private
boolean
isCanPullDown() {
return
getScrollY() ==
0
||
contentView.getHeight() < getHeight() + getScrollY();
}
/**
* 判断是否滚动到底部
*/
private
boolean
isCanPullUp() {
return
contentView.getHeight() <= getHeight() + getScrollY();
}
在用户按下手指时(也就是ACTION_DOWN事件), 调用上面的两个方法判断是否滚动到了顶部或底部, 如果滚动到了顶部或底部, 就说明在移动手指时需要移动contentView的布局。 这时就用标志位canPullDown和canPullUp 记住这两个个状态, 并且也记住ACTION_DOWN发生时, 手指触摸点的Y值, 就是startY成员变量。具体实现代码如下:
case
MotionEvent.ACTION_DOWN:
//判断是否可以上拉和下拉
canPullDown = isCanPullDown();
canPullUp = isCanPullUp();
//记录按下时的Y值
startY = ev.getY();
break
;
2 布局的移动
如果上面一步执行ACTION_DOWN之后, 判定不处于上拉或下拉的时机上, 那么在ACTION_MOVE事件处理时, 也要随着ACTION_MOVE事件的多次触发持续更新手指所处的Y值(startY变量)并且及时判断在手指移动的过程中是否使contentView滚动到了顶部或底部, 如果使contentView滚动到了顶部或底部,那么在下一个ACTION_MOVE事件的触发点, 就要移动布局了。 如果上面一步执行ACTION_DOWN之后, 就已经确定要上拉或下拉布局, 那么在ACTION_MOVE时, 也就要随着手指的移动而移动布局。
布局移动的实现原理是改变contentView中的mTop和mBottom成员变量的值(这两个变量定义在父类View中),并且对contentView重新布局。 mTop代表当前控件的顶端到父控件的顶端的距离,mBottom代表当前控件的底端到父控件的顶端的距离 。示意图如下:
同时改变mTop和mBottom的值, 可以使contentView上下移动, 达到随手指拉伸的效果。代码逻辑如下:
case
MotionEvent.ACTION_MOVE:
//在移动的过程中, 既没有滚动到可以上拉的程度, 也没有滚动到可以下拉的程度
if
(!canPullDown && !canPullUp) {
startY = ev.getY();
canPullDown = isCanPullDown();
canPullUp = isCanPullUp();
break
;
}
//计算手指移动的距离
float
nowY = ev.getY();
int
deltaY = (
int
) (nowY - startY);
//是否应该移动布局
boolean
shouldMove =
(canPullDown && deltaY >
0
)
//可以下拉, 并且手指向下移动
|| (canPullUp && deltaY<
0
)
//可以上拉, 并且手指向上移动
|| (canPullUp && canPullDown);
//既可以上拉也可以下拉(这种情况出现在ScrollView包裹的控件比ScrollView还小)
if
(shouldMove){
//计算偏移量
int
offset = (
int
)(deltaY * MOVE_FACTOR);
//随着手指的移动而移动布局
contentView.layout(originalRect.left, originalRect.top + offset,
originalRect.right, originalRect.bottom + offset);
isMoved =
true
;
//记录移动了布局
}
break
;
上面代码有三点需要注意: 1 MOVE_FACTOR是一个常量, 定义为0.5F。 这是一个因子, 让手指移动的举例乘以这个因子得到布局移动的距离, 例如手指移动了100px, 那么布局就移动100*0.5 = 50px。 这样做主要是达到一种延迟的效果, 增强用户的体验。 2 调用contentView的layout方法对他重新布局, 传入的originalRect.top + offset 和 originalRect.bottom + offset就是新的mTop和mBottom, originalRect.top和originalRect.bottom是原始的mTop和mBottom。 3 isMove变量用于记住布局已经移动的状态, 以便于在ACTION_UP事件触发时, 将布局回弹到正常位置
3 布局的回弹
在用户松开手时, 布局要回弹到原始位置。这个回弹很简单, 就是让mTop和mBottom回到原始的值。并加上动画效果。mTop和mBottom的原始值被记录在一个名为originalRect的Rect对象中。 在对ScrollView进行布局操作的时候, 初始化这个originalRect对象, 代码如下:
@Override
protected
void
onLayout(
boolean
changed,
int
l,
int
t,
int
r,
int
b) {
super
.onLayout(changed, l, t, r, b);
if
(contentView ==
null
)
return
;
//ScrollView中的唯一子控件的位置信息, 这个位置信息在整个控件的生命周期中保持不变
originalRect.set(contentView.getLeft(), contentView.getTop(), contentView
.getRight(), contentView.getBottom());
}
该方法首先调用父类的同名方法对ScrollView进行布局,在ScrollView进行布局时, 会对他的子View进行递归布局操作, 也就是说, 在调用完父类的onLayout方法后, contentView也已经完成了布局操作, 这时它的位置是可以确定的。 所以下面就将它的位置信息保存在originalRect对象中。
回弹效果的实现逻辑如下:
case
MotionEvent.ACTION_UP:
if
(!isMoved)
break
;
//如果没有移动布局, 则跳过执行
// 开启动画
TranslateAnimation anim =
new
TranslateAnimation(
0
,
0
, contentView.getTop(),
originalRect.top);
anim.setDuration(ANIM_TIME);
contentView.startAnimation(anim);
// 设置回到正常的布局位置
contentView.layout(originalRect.left, originalRect.top,
originalRect.right, originalRect.bottom);
//将标志位设回false
canPullDown =
false
;
canPullUp =
false
;
isMoved =
false
;
break
;
上面代码中ANIM_TIME是一个常量, 代表动画的执行时间, 被定义为300毫秒。 回弹到原始位置后, 将canPullDown,canPullUp和 isMoved标志清空, 以便进行下一次的触摸事件周期。
其他实现方式
在网上搜寻解决方案时, 发现以下代码也能实现弹性效果。
public
class
ReboundScrollView
extends
ScrollView {
private
static
final
int
MAX_Y_OVERSCROLL_DISTANCE =
500
;
private
Context mContext;
private
int
mMaxYOverscrollDistance;
public
ReboundScrollView(Context context){
super
(context);
mContext = context;
initBounceScrollView();
}
public
ReboundScrollView(Context context, AttributeSet attrs){
super
(context, attrs);
mContext = context;
initBounceScrollView();
}
public
ReboundScrollView(Context context, AttributeSet attrs,
int
defStyle){
super
(context, attrs, defStyle);
mContext = context;
initBounceScrollView();
}
private
void
initBounceScrollView(){
final
DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
final
float
density = metrics.density;
mMaxYOverscrollDistance = (
int
) (density * MAX_Y_OVERSCROLL_DISTANCE);
}
@Override
protected
boolean
overScrollBy(
int
deltaX,
int
deltaY,
int
scrollX,
int
scrollY,
int
scrollRangeX,
int
scrollRangeY,
int
maxOverScrollX,
int
maxOverScrollY,
boolean
isTouchEvent){
//这块是关键性代码
return
super
.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, mMaxYOverscrollDistance, isTouchEvent);
}
}
实现原理就是通过overScrollBy方法设置ScrollView可以过度滚动。 这种实现虽然简单, 但是有以下几个缺陷:
1 下拉或上拉时, 无法实现延迟效果, 也就是手指移动100px, 那么布局也移动100px, 经过尝试, 这种方式体验并不好, 给人的感觉是控件活动太灵活了。
2 如果下拉或上拉的举例超过 MAX_Y_OVERSCROLL_DISTANCE设定的值, 布局就不会再随着手指的移动而移动
3 无法设置自定义的动画, 不能控制动画持续的时间
效果演示
下面是演示的截图。
如果在触摸前已经滚动到顶部, 效果图如下:
如果按住屏幕下拉, 会出现以下效果:
<img src="http://www.2cto.com/uploadfile/Collfiles/20140218/20140218092642242.png" alt="n峨跰坚黑喎�" http:="" www.2cto.com="" soft"="" target="_blank" class="keylink" style="border-width: 0px; padding: 0px; margin: 0px; list-style: none; color: rgb(51, 51, 51); font-family: 宋体; font-size: 14px; line-height: 28px; width: 240px; height: 426px;">下载我上传到百度云盘的资源。 该资源是一个简单的Android工程, 里面同样有ReboundScrollView的源码。
资源链接: http://pan.baidu.com/s/1sj97qqD
- Android上实现仿IOS弹性ScrollView
- Android上实现仿IOS弹性ScrollView
- Android上实现仿IOS弹性ScrollView
- Android上实现仿IOS弹性ScrollView
- Android上实现仿IOS弹性ScrollView
- Android上实现仿IOS弹性ScrollView
- Android上实现仿IOS弹性ScrollView
- Android上实现仿IOS弹性ScrollView
- Android上仿IOS弹性ScrollView
- Android 之实现仿IOS弹性ScrollView
- Android仿IOS上拉/下拉弹性效果ScrollView
- Android上实现仿IOS弹性ListView
- 仿IOS弹性ScrollView
- Android上实现弹性ScrollView
- android仿ios弹性
- Android仿IOS上拉下拉弹性效果
- Android仿IOS上拉下拉弹性效果
- android仿ios弹性页
- 清除APK缓存和获取APK的数据大小
- html table 表头 分割斜线
- Android FAQ - ffmpeg
- 使用monitor command监控QEMU运行状态
- 线性表
- Android上实现仿IOS弹性ScrollView
- 基于核心价值和场景的测试胜过测试用例
- android NFC学习笔记(三)
- 操作 sqlite数据库 Warning: there is at least one open result set around after performing [FMDatabaseQueue
- 目标板挂载NFS时 需要nolock参数
- html 用js框架
- 判断网络连接并跳到设置界面
- 高性能js模板引擎(artTemplate )
- 白话数字签名