使用各种App的时候,发现一个特点,现在的App在可以滑动的页面中,基本上都添加了一个置顶按钮的效果,使的用户可以很方便的回到顶部,就像网页中几乎所有的页面都有置顶效果按钮一样,觉得这个交互不错,以后自己的项目里也肯定会用到的,就抽空把这个小功能实现了一下,现在把实现过程记录下,以方便以后使用。
先看下效果图:
当ScrollView向上滑动超过一定距离后,就渐变的出现一个置顶的按钮,当滑动距离小于我们指定的距离时,按钮又以一个渐变的方式消失。
实现原理:
这个小案例的实现原理很简单:1.就是监听View的onScrollChanged()方法,获取到ScrolView滑动的距离,如果大于我们的距离,则出现置顶按钮,否则,隐藏
2.按钮点击事件的处理,点击按钮让整个ScrollView滑动到(0,0)位置即可。
虽然这个案例很简单,但是却涉及到了android的滑动原理,从onScrollViewChanged这个方法属于View这个类也可以看出,android的任何View都是可以滑动的,在这个案例后,我会说一下我对android滑动原理的理解。
自定义ScrollView代码:
<span style="font-size:14px;">
/**********************************************************
* @文件名称:GoTopScrollView.java
* @文件作者:rzq
* @文件描述:滑动超过一定距离后,出现置顶按钮
* @修改历史:2015年3月26日创建初始版本
**********************************************************/
public class GoTopScrollView extends ScrollView implements OnClickListener
{
private ImageView goTopBtn;
private int screenHeight;
public GoTopScrollView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public void setScrollListener(ImageView goTopBtn)
{
this.goTopBtn = goTopBtn;
this.goTopBtn.setOnClickListener(this);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt)
{
super.onScrollChanged(l, t, oldl, oldt);
/**
* 滑动距离超过300px,出现向上按钮,可以做为自定义属性
*/
if (t >= 300)
{
goTopBtn.setVisibility(View.VISIBLE);
}
else
{
goTopBtn.setVisibility(View.GONE);
}
}
@Override
public void onClick(View v)
{
if (v.getId() == R.id.go_top_btn)
{
this.smoothScrollTo(0, 0);
}
}
}</span>
Activity使用代码:
<span style="font-size:14px;">private void initView()
{
container = (RelativeLayout) findViewById(R.id.container);
container.setLayoutTransition(new LayoutTransition());
goTopBtn = (ImageView) findViewById(R.id.go_top_btn);
scrollView = (GoTopScrollView) findViewById(R.id.scroll_view);
scrollView.setScrollListener(goTopBtn);
}</span>
这样就很轻松的实现了这个置顶的效果,但仅仅这样是不够的,现在我们来分析一下android的滑动原理到底是怎么回事,是不是只有ScrollView才可以滑动。
1.首先,android中任意的View,都是可以滑动的,为什么呢?请看View中的关键源码,只挑出和滑动有关的代码:
<span style="font-size:14px;"> /**
* 指定X轴的滑动,Y轴不动
* @param value the x position to scroll to
*/
public void setScrollX(int value) {
scrollTo(value, mScrollY);
}
/**
* 指定Y轴的滑动,X轴不动
* @param value the y position to scroll to
*/
public void setScrollY(int value) {
scrollTo(mScrollX, value);
}
/**
* 真正处理滑动的方法
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
/**
* 触发滑动后的监听
* @param l Current horizontal scroll origin.
* @param t Current vertical scroll origin.
* @param oldl Previous horizontal scroll origin.
* @param oldt Previous vertical scroll origin.
*/
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
postSendViewScrolledAccessibilityEventCallback();
}
mBackgroundSizeChanged = true;
final AttachInfo ai = mAttachInfo;
if (ai != null) {
ai.mViewScrollChanged = true;
}
}</span><span style="font-size:14px;">
</span>
由这几个方法可以看出,android中所有View都是可以滑动的,其实所有的View都有两套坐标系,一个是指定View位置的X,Y,另一对则是指定View滑动坐标系,mScrollX和mScrollY,两对坐标系统是完全独立的,即X,Y坐标的改变不会影响mScrollX和mScrollY,反之也成立。下面,我们则重点分析一下scrollTo这个方法。
scrollTo(int x, int y) 是将View中内容滑动到相应的位置,参考的坐标系原点为parent View的左上角。
调用scrollTo(100, 0)表示将View中的内容移动到x = 100, y = 0的位置,如下图所示。注意,图中黄色矩形区域表示的是一个parent View,绿色虚线矩形为parent view中的内容。一般情况下两者的大小一致,本文为了显示方便,将虚线框画小了一点。图中的黄色区域的位置始终不变,发生位置变化的是显示的内容,如图所示:
mScrollX和mScrollY是View类中专门用于记录滑动位置的变量。这两个函数最终调用onScrollChanged()函数。
先总结一下,android的滑动原理就是:通过调用scrollTo(),使的View的滑动坐标系发生改变,并保存在mScrollX和mScrollY这两个成员变量中,我们可以通过getScrollX()和getScrollY(),获取滑动坐标值。
由图中我们可以看到,当我们调用scrollTo(100,0);时,发现View是向左滑动了,传入一个正数滑动的方向却是向左,这与我们平常理解的坐标轴是相反的,其实并不矛盾,因为滑动坐标系本来就与我们平常的坐标系统不一样,调用scrollTo方法,最终会执行到以下代码:
public void invalidate(int l, int t, int r, int b) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);
}
if (skipInvalidate()) {
return;
}
if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS) ||
(mPrivateFlags & DRAWING_CACHE_VALID) == DRAWING_CACHE_VALID ||
(mPrivateFlags & INVALIDATED) != INVALIDATED) {
mPrivateFlags &= ~DRAWING_CACHE_VALID;
mPrivateFlags |= INVALIDATED;
mPrivateFlags |= DIRTY;
final ViewParent p = mParent;
final AttachInfo ai = mAttachInfo;
//noinspection PointlessBooleanExpression,ConstantConditions
if (!HardwareRenderer.RENDER_DIRTY_REGIONS) {
if (p != null && ai != null && ai.mHardwareAccelerated) {
// fast-track for GL-enabled applications; just invalidate the whole hierarchy
// with a null dirty rect, which tells the ViewAncestor to redraw everything
p.invalidateChild(this, null);
return;
}
}
if (p != null && ai != null && l < r && t < b) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
final Rect tmpr = ai.mTmpInvalRect;
tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY);
p.invalidateChild(this, tmpr);
}
}
}
看倒数第五行代码,更新滑动坐标系时,走的是 tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY);传入一个正数后一减,就成了负数,自然要向左移动了,这样就又统一起来了。
总结:以上差不多就是View自带的滑动原理,但View中的滑动都是瞬时滑动。没有任何的过渡效果,结合Scroller是可以做出各种各样的过渡效果的,下篇文章,总结一下如何结合Scroller使View更加炫的滑动。
参考文章: 图解android View的scroll原理