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原理

                     


 

  

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。