android自定义View之自定义可置顶ScrollView,View滑动原理简析
使用各种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原理
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。