Android UI设计之自定义ScrollView,实现QQ空间阻尼下拉刷新和渐变菜单栏效果

        转载请注明出处:http://blog.csdn.net/llew2011/article/details/52626148

        之前写过一篇Android UI设计之<十>自定义ListView,实现QQ空间阻尼下拉刷新和渐变菜单栏效果的文章,写完那篇文章后想趁热打铁再写一篇用ScrollView来实现同样效果的文章,可是写了点开头就没有继续写下去了,当时想的是等用到再写吧,于是把它扔在了草稿箱中。近来恰好有用到,赶紧就把该文章补充完整发表出来,希望能给大家一点帮助......

        使用ScrollView来实现QQ空间的阻尼下拉刷新和渐变菜单栏效果的原理和Android UI设计之<十>自定义ListView,实现QQ空间阻尼下拉刷新和渐变菜单栏效果的原理是一样的都是用到了Android 2.3版本后的overScrollBy()方法,如果你不熟悉该方法请阅读上篇文章,在上篇文章中我对该方法做了介绍或者是小伙伴们自行google。现在我们先看一下效果吧:

        阅读到这里希望你已经看了我写的这篇同类文章,没看过也不要紧,我会带着小伙伴们一步一步的来实现我们想要的效果,首先我们看一下QQ空间的运行效果,当滚动到最顶部后,这时候如果我们手指继续下滑,则最顶部的View出现拉伸的效果,如果我们手指离开屏幕,则刚刚拉伸的View出现了阻尼回弹效果,当我们往上滚动时菜单栏就会随着滚动距离的增大其透明度逐渐增大直到完全不透明,反之逐渐透明。这样的体验感觉很棒有木有?说实话我是非常喜欢QQ的用户体验,平时也喜欢模仿QQ的各种特效,撤远了(*^__^*) ……

        实现QQ空间运行效果前需要考虑两个问题:

  • 如何实现菜单透明度渐变
            通过观察QQ空间的运行效果可知其菜单栏默认为透明,随着滚动距离变化而变化,要想实现透明度的变化就要知道ScrollView的滚动距离,所以有关透明度的问题也就转化成了滚动距离的问题。
  • 如何实现阻尼下拉和回弹效果
            要想利用ScrollView实现阻尼效果就要求ScrollView首先滚动到了顶部,当ScrollView滚动到了顶部之后若继续手动下滑就要求其第一个Child变化来模拟下拉效果,当手指离开屏幕后该Child要恢复到初始状态。

        我们先看第一个问题:要想实现透明度渐变就要先获取到ScrollView的滚动距离,通过滚动距离来计算出相应的透明度。在上篇文章中由于ListView的复用机制导致没法直接的获取到滚动距离,因此当时采用了addHeaderView()的方式,但是在ScrollVie中我们可以直接调用getScrollY()方法来获取滚动距离,因为该方法的返回值就是当前的滚动距离,有了滚动距离我们就可以计算出透明度了,所以在ScrollView中第一个问题那就不是事(*^__^*) ……

        我们先实现菜单栏透明度渐变的功能。定义自己的ScrollView,取名为FlexibleScrollView,单词flexible是灵活的、多样的的意思,因为我们的ScrollView不仅要实现菜单栏的透明度渐变还要实现阻尼效果,所以取名为FlexibleScrollView比较恰当。FlexibleScrollView继承ScrollView后需要实现其构造方法,代码如下:

public class FlexibleScrollView extends ScrollView {

    public FlexibleScrollView(Context context) {
        super(context);
    }

    public FlexibleScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlexibleScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

        FlexibleScrollView仅仅是继承了ScrollView,这本质上和ScrollView没有区别。由于在ScrollView中可以直接通过getScrollY()方法获取到滚动距离,所以接下来就是判断ScrollView的滚动时机,在上篇文章中我们知道ListView发生滚动时总会调用onScrollChanged()方法,因此我们重写了onScrollChanged()方法来计算透明度,那我们在FlexibleScrollView中是否还能重写该方法呢?答案是OK的,熟悉ScrollView的滚动原理的童靴们应该清楚,ScrollView的滚动可分为两部分,一部分是手指触摸屏幕触发的滚动,另一部分是手指离开屏幕可能发生的滚动。

        我们先看手指触摸屏幕触发的滚动时机,其源码如下:

@Override
public boolean onTouchEvent(MotionEvent ev) {
    ......

    switch (action & MotionEvent.ACTION_MASK) {
        ......
            
        case MotionEvent.ACTION_MOVE:
            if (mIsBeingDragged) {
                
                ......

                /** 在这里调用了onScrollChanged()方法 **/
                onScrollChanged(mScrollX, mScrollY, oldX, oldY);

                
                ......
            }
            break;
        ......
            
    }
    return true;
}

        根据源码我们知道在ScrollView的onTouchEvent()方法中当发生了ACTION_MOVE事件后总会调用onScrollChanged()方法,所以重写该方法似乎是可行的。

        接着我们再看一下当手指离开屏幕后发生的情况,源码如下:

@Override
public boolean onTouchEvent(MotionEvent ev) {
    ......

    switch (action & MotionEvent.ACTION_MASK) {
        ......
        
        case MotionEvent.ACTION_UP:
            if (mIsBeingDragged) {
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);

                if (getChildCount() > 0) {
                    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                        fling(-initialVelocity);
                    } else {
                        if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
                                getScrollRange())) {
                            invalidate();
                        }
                    }
                }

                mActivePointerId = INVALID_POINTER;
                endDrag();
            }
            break;
        ......
            
    }
    return true;
}
        手指离开屏幕时会触发ACTION_UP事件中,在ACTION_UP事件中,首先计算在Y轴上的滚动速度,如果手指离开屏幕时在Y轴方向上的滚动速度大于最小滚动速度时就会调用fling()方法,我们看一下fling()方法,源码如下:
/**
 * Fling the scroll view
 *
 * @param velocityY The initial velocity in the Y direction. Positive
 *                  numbers mean that the finger/cursor is moving down the screen,
 *                  which means we want to scroll towards the top.
 */
public void fling(int velocityY) {
    if (getChildCount() > 0) {
        int height = getHeight() - mPaddingBottom - mPaddingTop;
        int bottom = getChildAt(0).getHeight();

        mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
                Math.max(0, bottom - height), 0, height/2);

        final boolean movingDown = velocityY > 0;

        if (mFlingStrictSpan == null) {
            mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
        }

        invalidate();
    }
}
        在fling()方法中,参数velocityY表示在Y轴的速度(这里补充一知识点,如果嫌ScrollView滚动过快可以重写该方法来实现),在该方法中调用了mScroller的fling()方法,mScroller为OverScroller类型,该类很重要,在自定义ViewGroup中特别是实现滚动的时候有不可替代的作用,有对该方法不熟悉的小伙伴请自行查阅,这里就不再详解了。mScroller调用完fling()方法之后通过invalidate()方法刷新页面,刷新页面后又辗转调用了computeScroll()方法,该方法源码如下:

@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {

        int oldX = mScrollX;
        int oldY = mScrollY;
        int x = mScroller.getCurrX();
        int y = mScroller.getCurrY();

        if (oldX != x || oldY != y) {
            final int range = getScrollRange();
            final int overscrollMode = getOverScrollMode();
            final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                    (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

            overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
                    0, mOverflingDistance, false);

            // 在这里调用了onScrollChanged()方法
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);

            if (canOverscroll) {
                if (y < 0 && oldY >= 0) {
                    mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
                } else if (y > range && oldY <= range) {
                    mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
                }
            }
        }

        awakenScrollBars();

        // Keep on drawing until the animation has finished.
        postInvalidate();
    } else {
        if (mFlingStrictSpan != null) {
            mFlingStrictSpan.finish();
            mFlingStrictSpan = null;
        }
    }
}

        可以看到在computeScroll()方法中也调用了onScrollChanged()方法,因此根据以上源码分析可知ScrollView发送滚动时一定回调onScrollChanged()方法的,所以通过重写该方法来计算透明度是可行的。

        好了,经过一系列的源码分析我们知道ScrollView的滚动调用了onScrollChanged()方法,所以我们就重写该方法来计算透明度,要计算透明度我们就要知道是谁的透明度要渐变,而渐变的本质就是菜单栏View的透明度发生变化,透明度的渐变是通过alpha来控制的,所以我们需要定义表示菜单栏的背景属性mActionBarBackground,还需要定义一个最大的滚动距离常量值mMaxScrollHeight,通过滚动距离和此最大值来计算出当前所对应的透明度,所以代码如下:

public class FlexibleScrollView extends ScrollView {

    private static final int DEFAULT_SCROLL_HEIGHT = 500;
    
    private Drawable mActionBarBackground;
    private int mMaxScrollHeight = DEFAULT_SCROLL_HEIGHT;

    public FlexibleScrollView(Context context) {
        super(context);
    }

    public FlexibleScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlexibleScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if(null != mActionBarBackground) {
            mActionBarBackground.setAlpha(evaluateAlpha(Math.abs(getScrollY())));
        }
    }
    
    public void bindActionBar(View actionBar) {
        if(null != actionBar) {
            mActionBarBackground = actionBar.getBackground();
            if(null == mActionBarBackground) {
                mActionBarBackground = new ColorDrawable(Color.TRANSPARENT);
            }
            mActionBarBackground.setAlpha(0);
            if(Build.VERSION.SDK_INT >= 16) {
                actionBar.setBackground(mActionBarBackground);
            } else {
            	actionBar.setBackgroundDrawable(mActionBarBackground);
            }
        }
    }
    
    private int evaluateAlpha(int t) {
        if (t >= mMaxScrollHeight) {
            return 255;
        }
        return (int) (255 * t /(float) mMaxScrollHeight);
    }
}

        在FlexibleScrollView中我们定义了mActionBarBackground,它表示菜单栏的背景;mMaxScrollHeight表示滚动的最大距离(默认值设定为500像素),当ScrollView的滚动值超过了这个最大滚动值,就把菜单栏的透明度设置为不透明否则通过调用evaluateAlpha()方法计算出当前所对应的透明度。

        现在菜单栏的透明度功能准备就绪了,我们先测试一下看看效果,定义菜单栏布局文件action_bar_layout.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/action_bar_height"
    android:background="#aabbcc"
    android:clickable="true"
    android:orientation="vertical"
    android:paddingLeft="10dp">

    <TextView
        android:id="@+id/back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:drawableLeft="@drawable/back"
        android:text="动态"
        android:textColor="#b8e7fe"
        android:textSize="17sp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="好友动态"
        android:textColor="#b8e7fe"
        android:textSize="17sp" />

</FrameLayout>
        菜单栏包含一个返回按钮和一个标题,并且给菜单栏设置了固定高度和背景色,然后布局我们的activity_main.xml文件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.llew.sx.scroll.FlexibleScrollView
        android:id="@+id/flexible_scroll_vew"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fadingEdge="none"
        android:fadingEdgeLength="0dp"
        android:fillViewport="true"
        android:scrollbars="none" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >

            <include
                android:id="@+id/flexible_header_view"
                layout="@layout/flexible_header_layout" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#bbaacc"
                android:gravity="center"
                android:text="我是第一行" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#aaccbb"
                android:gravity="center"
                android:text="我是第二行" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#bbccaa"
                android:gravity="center"
                android:text="我是第三行" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#ccaabb"
                android:gravity="center"
                android:text="我是第四行" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#bcabac"
                android:gravity="center"
                android:text="我是第五行" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#baccba"
                android:gravity="center"
                android:text="我是第六行" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#abaccb"
                android:gravity="center"
                android:text="我是第七行" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#bcbcaa"
                android:gravity="center"
                android:text="我是第八行" />
        </LinearLayout>
    </com.llew.sx.scroll.FlexibleScrollView>

    <include
        android:id="@+id/custom_action_bar"
        layout="@layout/action_bar_layout" />

</FrameLayout>
        activity_main.xml的布局文件封简单,采用FrameLayout根布局让菜单栏悬浮在FlexibleListView上边,在include标签中引入的布局为了省时间我就直接复用了 上篇文章的布局文件,其代码如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/header_height">

    <ImageView
        android:id="@+id/iv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        android:src="@drawable/ttt" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:layout_gravity="bottom"
        android:background="#33333333"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <TextView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="相册"
            android:gravity="center"
            android:textColor="@android:color/white" />

        <View
            android:layout_width="1dp"
            android:layout_height="20dp"
            android:background="#ffffff" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="说说"
            android:gravity="center"
            android:textColor="@android:color/white" />

        <View
            android:layout_width="1dp"
            android:layout_height="20dp"
            android:background="#ffffff" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="个性化"
            android:gravity="center"
            android:textColor="@android:color/white" />

        <View
            android:layout_width="1dp"
            android:layout_height="20dp"
            android:background="#ffffff" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="\@ 与我相关"
            android:gravity="center"
            android:textColor="@android:color/white" />

    </LinearLayout>
</FrameLayout>
        然后接着编写我们的MainActivity代码,如下所示:
public class MainActivity extends Activity {

	private FlexibleScrollView mScrollView;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		mScrollView = (FlexibleScrollView) findViewById(R.id.flexible_scroll_vew);
		mScrollView.bindActionBar(findViewById(R.id.custom_action_bar));
	}
}
        在MainActivity中通过调用FlexibleScrollView的bindActionBar()方法把悬浮菜单的背景赋值给了FlexibleScrollView的mActionBarBackground。该测试代码很简单,我们运行一下程序,看看效果:

        看到运行效果好开心呀,(*^__^*) ……透明度渐变达到了我们的预期,接下来开始实现阻尼效果,阻尼效果就是当ScrollView滚动到了顶部此时若继续下滑,ScrollView能够继续往下滚动一段距离当手指离开屏幕后ScrollView要恢复原位置。假如你看过上篇文章就应该明白我们今天实现这个功能同样是通过重写overScrollBy()方法(若小伙伴们有对该方法不熟悉的请点击这里,在这篇文章中我有对该方法做了讲解)。

        实现阻尼效果的核心就是重写overScrollBy()方法,在该方法中改变HeaderView的高度,若手指松开我们就复原HeaderView。我们知道QQ空间顶部是一张图片,当下拉的时候该图片有弹性拉升效果,当手指松开后图片又伸缩回去了,所以我们就直接用ImageView模拟此效果。模拟图片阻尼可以让ImageView的宽高为MATCH_PARENT(HeaderView的高度改变之后ImageView的高度也可以随之更改),这个时候还要设置ImageView的scaleType为CENTER_CROP(不清楚ImageView的scaleType属性可参照我之前写的一篇博文:Android 源码系列之<一>从源码的角度深入理解ImageView的ScaleType属性)。

        现在开始在修改我们的FlexibleScrollView,代码如下:

public class FlexibleScrollView extends ScrollView {

    private static final float DEFAULT_LOAD_FACTOR = 3.0F;
    private static final int DEFAULT_SCROLL_HEIGHT = 500;

    private View mHeaderView;
    private int mOriginHeight;
    private int mZoomedHeight;
    
    private Drawable mActionBarBackground;
    private int mMaxScrollHeight = DEFAULT_SCROLL_HEIGHT;

    public FlexibleScrollView(Context context) {
        super(context);
    }

    public FlexibleScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlexibleScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if(null != mActionBarBackground) {
            mActionBarBackground.setAlpha(evaluateAlpha(Math.abs(getScrollY())));
        }
    }

	@Override
    protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
        if(null != mHeaderView) {
            if(isTouchEvent && deltaY < 0) {
            	mHeaderView.getLayoutParams().height += Math.abs(deltaY / DEFAULT_LOAD_FACTOR);
            	mHeaderView.requestLayout();
                mZoomedHeight = mHeaderView.getHeight();
            }
        }
        return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
    }

    @SuppressLint("ClickableViewAccessibility")
	@Override
    public boolean onTouchEvent(MotionEvent ev) {
        if(null != mHeaderView && 0 != mOriginHeight && 0 != mZoomedHeight) {
            int action = ev.getAction();
            if(MotionEvent.ACTION_UP == action || MotionEvent.ACTION_CANCEL == action) {
                resetHeaderViewHeight();
            }
        }
        return super.onTouchEvent(ev);
    }
    
    public void bindActionBar(View actionBar) {
        if(null != actionBar) {
            mActionBarBackground = actionBar.getBackground();
            if(null == mActionBarBackground) {
                mActionBarBackground = new ColorDrawable(Color.TRANSPARENT);
            }
            mActionBarBackground.setAlpha(0);
            if(Build.VERSION.SDK_INT >= 16) {
                actionBar.setBackground(mActionBarBackground);
            } else {
            	actionBar.setBackgroundDrawable(mActionBarBackground);
            }
        }
    }
    
    public void setHeaderView(View headerView) {
    	this.mHeaderView = headerView;
    	updateHeaderViewHeight();
    }

    private void updateHeaderViewHeight() {
        mOriginHeight = null == mHeaderView ? 0 : mHeaderView.getHeight();
        if(0 == mOriginHeight && null != mHeaderView) {
            post(new Runnable() {
                @Override
                public void run() {
                    mOriginHeight = mHeaderView.getHeight();
                }
            });
        }
    }
    
    private int evaluateAlpha(int t) {
        if (t >= mMaxScrollHeight) {
            return 255;
        }
        return (int) (255 * t /(float) mMaxScrollHeight);
    }

	private void resetHeaderViewHeight() {
        if(mHeaderView.getLayoutParams().height != mOriginHeight) {
            ValueAnimator valueAnimator = ValueAnimator.ofInt(mZoomedHeight, mOriginHeight);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                	mHeaderView.getLayoutParams().height = (Integer) animation.getAnimatedValue();
                	mHeaderView.requestLayout();
                }
            });
            valueAnimator.setDuration(200);
            valueAnimator.start();
        }
    }
}

        在FlexibleScrollView中,mHeaderView表示需要进行阻尼拉伸效果的View,mOriginHeight表示该View的原始高度,该值的获取是通过updateHeaderViewHeight()方法来获取的,mZoomedHeight表示该View拉伸后的高度,该值是在overScrollBy()方法中赋值的,DEFAULT_LOAD_FACTOR表示增长因子,目的是让mHeaderView缓慢增大。最后在onTouchEvent()方法中当手指离开屏幕时回调了resetHeaderViewHeight()方法复原该View的高度。

        接着修改我们的MainActivity,代码如下:

public class MainActivity extends Activity {

	private FlexibleScrollView mScrollView;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		mScrollView = (FlexibleScrollView) findViewById(R.id.flexible_scroll_vew);
		mScrollView.bindActionBar(findViewById(R.id.custom_action_bar));
		mScrollView.setHeaderView(findViewById(R.id.flexible_header_view));
	}
}

        下面我们运行一下,看看效果:

        恩,效果看上去还不错........

        好了,有关实现QQ空间的阻尼下拉刷新和渐变菜单栏就结束了,主要是利用了2.3版本之后的overScrollBy()方法(如果要兼容2.3之前版本需要童靴们自己去实现相关逻辑);其次充分的利用了ImageView的ScaleType属性来模拟了QQ空间图片阻尼回弹的效果。再次感谢收看(*^__^*) ……



        源码下载



发布了39 篇原创文章 · 获赞 87 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/llew2011/article/details/52626148