
通过观察QQ空间的运行效果,发现当往上滚动时菜单栏会随着滚动距离的增大其透明度组件增大直到完全不透明,反之逐渐透明。当滚动到顶部后继续下拉会出现拉升效果当松手之后出现阻尼回弹效果。于是就通过重写ListView模仿了QQ空间的运行效果。
实现QQ空间运行效果前需要考虑两个问题:
1)、如何实现菜单栏透明度渐变
通过观察QQ空间的运行效果可知其菜单栏的透明度是根据滚动距离而动态变化的,要想实现透明度的变化就需要知道的ListView的滚动距离,所以有关透明度的问题也就转化成了滚动距离的问题。
2)、如何实现阻尼拉升和回弹效果
要想利用ListView实现阻尼效果就要求ListView首先滚动到了顶部,当ListView滚动到了顶部之后若继续手动下滑就要求其第一个Child变化来模拟下拉效果,当手指松开后该Child要回弹到初始状态。
我们先看第一个问题:要想实现透明度渐变就要先获取到ListView的滚动距离,通过滚动距离来计算相应的透明度。由于ListView的复用机制就决定了不能通过第一个可见Item的getTop()方法来得到滚动值,所以我们可以通过HeaderView来获取滚动距离,因为Header在ListView中是不参与复用的。
下面先了解一下ListView添加HeaderView后的滚动流程:

上图大致画了ListView含有HeaderView时的三个滚动状态,状态一可称为初始状态或者是恰好滚动到最顶部状态,此时HeaderView的getTop()值为0;状态二为ListView的滚动中状态,此时HeaderView没有完全滚动出ListView边界,getTop()的返回值为负数且其绝对值范围在0和HeaderView的高度之间;状态三表示的是HeaderView完全滚动出了ListView边界,若调用getTop()得到的返回值为负数且绝对值等于HeaderView的高度(此后可理解成HeaderView一直固定在ListView的顶部)。
明白了ListView的滚动原理,我们先尝试实现渐变菜单栏的功能。首先定义自己的ListView,取名为FlexibleListView,单词flexible是灵活的、多样的的意思,因为我们的ListView不仅要实现菜单栏的透明度渐变还要实现阻尼效果,所以取名为FlexibleListView比较恰当。FlexibleListView继承ListView后需要实现其构造方法,代码如下:
public class FlexibleListView extends ListView { public FlexibleListView(Context context) {super(context);} public FlexibleListView(Context context, AttributeSet attrs) {super(context, attrs);} public FlexibleListView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);} @TargetApi(21)public FlexibleListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);} } FlexibleListView仅仅是继承了ListView,这本质上和ListView没有区别。既然我们是通过给ListView添加HeaderView的方式来判断滚动距离,那就要获取到HeaderView对象。怎么获取到HeaderView对象呢?这里有个技巧,由于给ListView添加HeaderView最终是调用ListView的addHeaderView(View v, Object data, boolean isSelected)方法,所以我们可以重写该方法,取到添加进来的第一个HeaderView,那怎么判断是第一个添加进来的HeaderView呢?因为HeaderView的添加是有序的即先添加的先绘制。所以可以定义一个代表第一个HeaderView的属性mHeaderView,当调用到addHeaderView()方法时通过判断mHeaderView的值是否为空,如果为空就赋值否则不赋值,代码如下:public class FlexibleListView extends ListView { private View mHeaderView;private int mMaxScrollHeight; public FlexibleListView(Context context) {super(context);} public FlexibleListView(Context context, AttributeSet attrs) {super(context, attrs);} public FlexibleListView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);} @TargetApi(21)public FlexibleListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);} @Overridepublic void addHeaderView(View v, Object data, boolean isSelectable) {super.addHeaderView(v, data, isSelectable);if(null == mHeaderView) {mHeaderView = v;mMaxScrollHeight = mHeaderView.getLayoutParams().height;}} } FlexibleListView中定义了mHeaderView和mMaxScrollHeight属性,在addHeaderView()方法中对mHeaderView做非空判断来获取到第一个HeaderView并赋值给mHeadereView,mMaxScrollHeight表示HeaderView的最大滚动距离,当HeaderView的滚动距离超过此值我们就要设置菜单栏不透明否则就更改透明度。在这里我直接使用了HeaderView的高度来表示其允许滚动的最大距离。/*** Notify our scroll listener (if there is one) of a change in scroll state*/ void invokeOnItemScrollListener() {if (mFastScroll != null) {mFastScroll.onScroll(mFirstPosition, getChildCount(), mItemCount);}if (mOnScrollListener != null) {mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);}onScrollChanged(0, 0, 0, 0); // dummy values, View"s implementation does not use these. } invokeOnItemScrollListener()方法就是触发滚动回调的,无论我们给不给ListView设置OnItemScrollListener那该方法都会调用,细心的同学可能发现在该方法最后调用了View的onScrollChanged()方法,这时候你恍然大悟,我们可以重写该方法呀,当ListView发生滚动了也就调用了onScrollChange()方法,多省事呀。呵呵,恭喜你,答对了,我们今天就是采用重写onScrollChanged()方法并在该方法中通过判断ListView的HeaderView的滚动距离来设置菜单栏的透明度的。public class FlexibleListView extends ListView { private View mActionBar;private View mHeaderView;private int mMaxScrollHeight;private Drawable mActionBarBackground; public FlexibleListView(Context context) {super(context);} public FlexibleListView(Context context, AttributeSet attrs) {super(context, attrs);} public FlexibleListView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);} @TargetApi(21)public FlexibleListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);} @Overrideprotected void onScrollChanged(int l, int t, int oldl, int oldt) {super.onScrollChanged(l, t, oldl, oldt);if(null != mActionBarBackground) {mActionBarBackground.setAlpha(evaluateAlpha(Math.abs(mHeaderView.getTop())));}} @Overridepublic void addHeaderView(View v, Object data, boolean isSelectable) {super.addHeaderView(v, data, isSelectable);if(null == mHeaderView) {mHeaderView = v;mMaxScrollHeight = mHeaderView.getLayoutParams().height;}} private int evaluateAlpha(int t) {if (t >= mMaxScrollHeight) {return 255;}return (int) (255 * t /(float) mMaxScrollHeight);} public void bindActionBar(View actionBar) {if(null != actionBar) {mActionBar = actionBar;mActionBarBackground = actionBar.getBackground();if(null == mActionBarBackground) {mActionBarBackground = new ColorDrawable(Color.TRANSPARENT);}mActionBarBackground.setAlpha(0);if(Build.VERSION.SDK_INT >= 16) {mActionBar.setBackground(mActionBarBackground);} else {mActionBar.setBackgroundDrawable(mActionBarBackground);}}} public void bindActionBar(ActionBar actionBar) {if(null != actionBar) {// TODO impl with ActionBar// actionBar.setBackgroundDrawable();}} } FlexibleListView新增了mActionBar和mActionBarBackground属性,mActionBar代表需要渐变的菜单栏,mActionBarBackground为菜单栏的背景。其次对外提供了重载方法bindActionBar(),参数为ActionBar的方法是空实现,里边添加了TODO提示符并给了setBackgroundDrawable()提示(注意ActionBar实现渐变需要设置WindowFeature),希望童靴们自己可以实现出来。<?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"> <TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:drawableLeft="@mipmap/back"android:text="动态"android:textColor="#b8e7fe"android:textSize="17sp" /> <TextViewandroid: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.wb.git.qqzone.FlexibleListViewandroid:id="@+id/flexible_list_view"android:layout_width="match_parent"android:layout_height="match_parent"android:scrollbars="none"></com.llew.wb.git.qqzone.FlexibleListView> <includeandroid:id="@+id/custom_action_bar"layout="@layout/action_bar_layout"/> </FrameLayout>activity_main.xml的布局文件很简单,采用FrameLayout根布局让菜单栏悬浮在FlexibleListView上边,然后编写我们的MainActivity代码,如下所示:
public class MainActivity extends AppCompatActivity { private FlexibleListView mListView; @Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main); initGlobalParams();} private void initGlobalParams() {mListView = (FlexibleListView) findViewById(R.id.flexible_list_view); View mFlexibleHeaderView = new View(getApplicationContext());mFlexibleHeaderView.setBackgroundColor(Color.parseColor("#bbaacc"));int height = getResources().getDimensionPixelSize(R.dimen.header_height);LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, height);mFlexibleHeaderView.setLayoutParams(params); final View actionBar = findViewById(R.id.custom_action_bar); mListView.bindActionBar(actionBar);mListView.addHeaderView(mFlexibleHeaderView); mListView.setAdapter(new Adapter());} static class Adapter extends BaseAdapter {@Overridepublic int getCount() {return 80;} @Overridepublic Object getItem(int position) {return null;} @Overridepublic long getItemId(int position) {return 0;} @Overridepublic View getView(int position, View convertView, ViewGroup parent) {TextView textView = new TextView(parent.getContext());textView.setPadding(50, 50, 50, 50);textView.setText(position + 10 + "");return textView;}} } 在MainActivity中给FlexibleListView添加了一个固定高度背景色为"#bbaacc"的Header,并把悬浮菜单栏actionBar赋值给了FlexibleListView的mActionBar,最后设置Adapter,为了测试代码写的很简单,我们运行一下程序,看看效果:
看到运行效果好开心呀,(*^__^*) ……透明度渐变功能达到了我们的预期,接下来开始实现阻尼效果,阻尼效果就是当ListView滚动到了顶部此时若继续下滑,ListView能够继续往下滚动一段距离当手指离开屏幕后ListView要恢复原位置。为了实现这个功能有的童靴可能会想到重写有关事件传递的onXXXEvent()等方法,之后在MotionEvent为DOWN,MOVE,UP或者CANCEL条件下分别做逻辑判断来实现阻尼效果,此方式可行,但是和今天我们的实现相比起来复杂了许多......
这里所实现阻尼效果所采用的方法是利用View的overScrollBy()方法,有的童靴可能会问overScrollBy()方法是2.3版本之后才增加的,2.3版本之前的兼容性怎么办?我实现这个功能之前也考虑过这个问题,一方面我们公司的APP只支持3.0以上版本,另一方面2.3及以前的版本市场占有率几乎微乎其微了,所以可以考虑不再兼容2.3以前的老版本。
有的同学或许对overScrollBy()方法比较陌生,先大致说一下该方法,其源码如下:
/*** Scroll the view with standard behavior for scrolling beyond the normal* content boundaries. Views that call this method should override* {@link #onOverScrolled(int, int, boolean, boolean)} to respond to the* results of an over-scroll operation.** Views can use this method to handle any touch or fling-based scrolling.** @param deltaX Change in X in pixels* @param deltaY Change in Y in pixels* @param scrollX Current X scroll value in pixels before applying deltaX* @param scrollY Current Y scroll value in pixels before applying deltaY* @param scrollRangeX Maximum content scroll range along the X axis* @param scrollRangeY Maximum content scroll range along the Y axis* @param maxOverScrollX Number of pixels to overscroll by in either direction* along the X axis.* @param maxOverScrollY Number of pixels to overscroll by in either direction* along the Y axis.* @param isTouchEvent true if this scroll operation is the result of a touch event.* @return true if scrolling was clamped to an over-scroll boundary along either* axis, false otherwise.*/ @SuppressWarnings({"UnusedParameters"}) protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY,int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {// ...... } 阅读源码看注释很重要,我们先看一下注释,大致意思如下:@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 / 3.0);mHeaderView.requestLayout();}}return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); } overScrollBy()方法中我们根据deltaY值动态的更改了mHeaderView的高度并重新布局达到更改ImageView高度的目的,注意:计算高度的时候用了deltaY除以3,此时的3表示增长因子,目的是让HeaderView缓慢的增长,这里可以对外提供一个方法来设置此值。@Override public boolean onTouchEvent(MotionEvent ev) {if(null != mHeaderView) {int action = ev.getAction();if(MotionEvent.ACTION_UP == action || MotionEvent.ACTION_CANCEL == action) {resetHeaderViewHeight();}}return super.onTouchEvent(ev); }private void resetHeaderViewHeight() {ValueAnimator valueAnimator = ValueAnimator.ofInt(1);valueAnimator.setDuration(700);valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {final float f = animation.getAnimatedFraction();mHeaderView.getLayoutParams().height -= f * (mHeaderView.getLayoutParams().height - mMaxScrollHeight);mHeaderView.requestLayout(); }});valueAnimator.setInterpolator(new OvershootInterpolator());valueAnimator.start(); } HeaderView的复原动画我们采用了ValueAnimator,当动画执行过程中我们动态的更改HeaderView的值来达到渐变效果。接下来布局HeaderView来模拟QQ空间,代码如下:<?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"> <ImageViewandroid:id="@+id/iv"android:layout_width="match_parent"android:layout_height="match_parent"android:scaleType="centerCrop"android:src="@mipmap/ttt" /> <LinearLayoutandroid:layout_width="match_parent"android:layout_height="30dp"android:layout_gravity="bottom"android:background="#33333333"android:gravity="center_vertical"android:orientation="horizontal"> <TextViewandroid:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:text="相册"android:gravity="center"android:textColor="@android:color/white" /> <Viewandroid:layout_width="1dp"android:layout_height="20dp"android:background="#ffffff" /> <TextViewandroid:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:text="说说"android:gravity="center"android:textColor="@android:color/white" /> <Viewandroid:layout_width="1dp"android:layout_height="20dp"android:background="#ffffff" /> <TextViewandroid:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:text="个性化"android:gravity="center"android:textColor="@android:color/white" /> <Viewandroid:layout_width="1dp"android:layout_height="20dp"android:background="#ffffff" /> <TextViewandroid:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:text="@ 与我相关"android:gravity="center"android:textColor="@android:color/white" /> </LinearLayout> </FrameLayout>HeaderView的布局中让ImageView的宽高都设置成了match_parent并且把scaleType设置为centerCrop。修改MainActivity的initGlobalParams()方法,代码如下:
void initGlobalParams() {mListView = (FlexibleListView) findViewById(R.id.flexible_list_view);View mFlexibleHeaderView = LayoutInflater.from(this).inflate(R.layout.flexible_header_layout, mListView, false);AbsListView.LayoutParams params = (AbsListView.LayoutParams)mFlexibleHeaderView.getLayoutParams();if(null == params) {params = new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.WRAP_CONTENT);}params.height = getResources().getDimensionPixelSize(R.dimen.header_height);mFlexibleHeaderView.setLayoutParams(params); final View actionBar = findViewById(R.id.custom_action_bar); mListView.bindActionBar(actionBar);mListView.addHeaderView(mFlexibleHeaderView); mListView.setAdapter(new Adapter()); } OK,一切都准备就绪,赶紧运行一下程序,看看效果吧(*^__^*) ……
恩,看上去效果还不错......
好了,有关实现QQ空间的阻尼下拉刷新和渐变菜单栏就结束了,主要是利用了2.3版本之后的overScrollBy()方法(如果要兼容2.3之前版本需要童靴们自己去实现相关逻辑);其次充分的利用了ImageView的ScaleType属性来模拟了QQ空间图片阻尼回弹的效果。再次感谢收看(*^__^*) ……
原文链接:http://blog.csdn.net/llew2011/article/details/51559694
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。