您的位置:首页 > 其它

如何实现列表滑动标签置顶效果(以天天动听、网易云音乐、虾米音乐的歌手页为例)

2015-08-06 23:57 621 查看
        很久前就准备写这篇技术博客了,无奈这两个月天天加班,现在总算抽出时间来好好写一写了~~

        当然三个app从产品角度来说歌手页面很像,具体谁抄谁的产品设计我们不去管(虽然个人很BS这种抄袭),那是他们的事,咱们来讲的是技术,讲的是实现。来吧,先看下界面,当然最好大家下载对应的app体验一下~

         体验后,天天动听和网易云音乐效果更佳,而虾米滑动时很大一个问题是,上滑至标签置顶时,无法继续滑动,必须松手再按下才能继续,下滑时也有同样的问题。

天天动听歌手页(V7.9.0正式版)(左为初始状态,右为标签置顶状态)


     


网易云音乐歌手页(V2.5.4)(左为初始状态,右为标签置顶状态)


       


虾米音乐(V4.6.3)(左为初始状态,右为标签置顶状态)


       


        从外表上看三个app好像都实现了需要的效果,很相似,但后面我们会发现其实从技术的角度来分析,三个app其实是大相径庭,特别是虾米音乐。

        在第一次看到这种效果的时候,相信你们和我的想法是一样的,首先这是一个ViewPager ,中间是一个tab标签,ViewPager的四个页面均包含一个ListView.

        那么问题来了:ViewPager到底是多高,是整个屏幕,还是去掉了头部图片剩下的高度?(如下图)图中为了看清两者区别,故意把viewPager画的宽了一点。



        其实按正常的思维来说,一般都会选择图B这种做法,理由很简单:1,ViewPager切换时,头部图片不会受到影响;2,标签置顶后,图片不再动,不会跟随列表滑上去。当然图B这种图片和列表分离的方式更容易做到这一点。

        事实上虾米应该就是这样做的,图B这种做法好处是容易理解且容易实现。但它却有一个致命的问题,就是前面提到的,用户在向上滑动时当标签置顶时,无法继续滑动,必须抬起手再按下才可以,为什么会出现这种情况呢?这里就不得不提一下事件的传递流程了~(如果对事件传递流程不熟,建议先看一下这位兄弟写的这篇好文章http://blog.csdn.net/xiaanming/article/details/21696315

        图B中的view事件传递如下图所示



        开始滑动时,ViewPager拦截了down事件,并消费了down事件(即ViewPager开始向上移动,此后的move和up事件除非手指抬起后再按下,否则不会再传递给ListView),当ViewPager滑动到顶端最终位置时,不再移动,此时抬起手ViewPager不拦截向下传递给ListView,ListView消费了touch事件,列表就正常滑动了。

        但一般的产品怎么能容忍这种体验(标签置顶后抬起手才能再次滑动),于是就有了新的思路,即按图A所示的方法。从一开始滑动到最后只有listview在消费事件,viewpager不拦截,这就不会出现图B方式的差体验。但图A这种图片怎么才能在标签置顶后不让它随列表滑动,才是这个方法的亮点,其实说穿了很简单,这个头部图片是盖在整个viewpager之上的,它只是在listview滑动时,动态改变imageview坐标位置或改变imageView高度即可,当然listview要加一个header,这个header可以是一个空view,高度与头部图片一样高即可,这样就给用户了一个错觉,觉的头部图片就是listview的header.到这里,问题就解决了。

        当然真正做这种效果时,你一定会遇到各种问题,比如当往上滑了一点距离后,再切换viewPager页面时,如何做到各个页面listview对齐;比如页面内容不足一屏时,如何还能滑动;再比如下拉放大效果和回弹效果的实现等。当然,如果你已经理解了图A的做法,那后面这些问题都可以一步步解决了。

        最后附上一段代码,自己实现的一个OnScrollListener,主要目的是类似于自定义控件,方便使用。(注:最好自己实现一个,因为这个listener可能并不适合你的需求)

import android.support.v4.view.MotionEventCompat;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.*;

/**
* Created by mie.shen
*/
public class MyOnScrollListener implements AbsListView.OnScrollListener {
private CallBack mC
4000
allBack;

private static int mHeadImageViewHeight; //px
private static int mHeadImageViewMaxHeight; //px
private static int mTabHostHeight; //px
private static final int TAB_MAX_ALPHA = 191;
private static final int SHADOW_MAX_ALPHA = 255;
private static final int ANIM_DELAY_TIME = 300;

/**
*
* @param headerImageHeight int
* @param headerImageMaxHeight int
* @param tabHeight int
* @param callBack CallBack
*/
public MyOnScrollListener(int headerImageHeight, int headerImageMaxHeight, int tabHeight, CallBack callBack) {
mHeadImageViewHeight = headerImageHeight;
mHeadImageViewMaxHeight = headerImageMaxHeight;
mTabHostHeight = tabHeight;
mCallBack = callBack;
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (currentListHeaderViewNotNull() && view == mCallBack.getCurrentPageListView()) {
view.setOnTouchListener(mOnTouchListener);
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
updateHeadLayout(view);
updateElementAlpha();
mCallBack.updateOtherPageListView();
}
}
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (view == mCallBack.getCurrentPageListView()) {
updateHeadLayout(view);
updateElementAlpha();
}
if (mCallBack.getCurrentPageListView() == view
&& isPageScrollEnable(firstVisibleItem, visibleItemCount, totalItemCount)) {
mCallBack.requestNextPage(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
}

private boolean isPageScrollEnable(int firstVisibleItem, int visibleItemCount, int totalItemCount) {
return (totalItemCount >= visibleItemCount) && ((totalItemCount - firstVisibleItem) <= visibleItemCount);
}

private void updateHeadLayout(AbsListView view) {
ViewGroup.LayoutParams params = mCallBack.getHeaderLayout().getLayoutParams();
if (params.height <= mHeadImageViewHeight) {
int scrollY = getScrollY(view);
params.height = mHeadImageViewHeight + Math.max(-scrollY, mTabHostHeight - mHeadImageViewHeight);
mCallBack.getHeaderLayout().setLayoutParams(params);
}
}

private int getScrollY(AbsListView view) {
View c = view.getChildAt(0);
if (c == null) {
return 0;
}

int firstVisiblePosition = view.getFirstVisiblePosition();
int top = c.getTop();

int headerHeight = 0;
if (firstVisiblePosition >= 1) {
headerHeight = mHeadImageViewHeight;
}
return -top + firstVisiblePosition * c.getHeight() + headerHeight;
}

private void updateElementAlpha() {
mCallBack.getTabBgView().getBackground().setAlpha((int)(TAB_MAX_ALPHA * (1.0 - getAlpha())));
mCallBack.getHeaderShadow().getBackground().setAlpha((int)(SHADOW_MAX_ALPHA * getAlpha()));
mCallBack.getIntroductionLayout().getBackground().setAlpha((int)(SHADOW_MAX_ALPHA * getAlpha()));
}

private float getAlpha() {
float alpha = (float)(mCallBack.getHeaderLayout().getHeight() - mTabHostHeight)
/ (mHeadImageViewHeight - mTabHostHeight);
return alpha > 1 ? 1 : alpha;
}

private View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {
private float mStartY = 0;
private int mImageNewHeight = mHeadImageViewHeight;
@Override
public boolean onTouch(View v, MotionEvent event) {
if (mCallBack.getHeaderLayout().getHeight() < mHeadImageViewHeight) {
return false;
}
if (event.getPointerCount() > 1) {
return true;
}
ListView view = (ListView)v;
switch (event.getAction() & MotionEventCompat.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mStartY = 0;
break;
case MotionEvent.ACTION_MOVE:
if (mStartY == 0 && mCallBack.getHeaderLayout().getHeight() == mHeadImageViewHeight) {
mStartY = event.getY();
mImageNewHeight = mHeadImageViewHeight;
mCallBack.updateOtherPageListView();
return false;
}
if (mStartY != 0 && mCallBack.getHeaderLayout().getHeight() >= mHeadImageViewMaxHeight && event.getY() >= mStartY) {
mStartY = event.getY();
mImageNewHeight = mHeadImageViewMaxHeight;
return true;
}
if (mStartY != 0 && mCallBack.getHeaderLayout().getHeight() >= mHeadImageViewHeight) {
enlargeHeaderLayout(view, event, mImageNewHeight, mStartY);
if (mCallBack.getHeaderLayout().getHeight() == mHeadImageViewHeight) {
return false;
}
return true;
}
break;
case MotionEvent.ACTION_UP:
if (mStartY != 0) {
mStartY = 0;
resetHeaderLayout(view.getChildAt(0), mCallBack.getHeaderLayout());
}
break;
default:
break;
}
return false;
}
};

private void enlargeHeaderLayout(ListView view, MotionEvent event, int imageNewHeight, float startY) {
AbsListView.LayoutParams listParams = (AbsListView.LayoutParams)view.getChildAt(0).getLayoutParams();
ViewGroup.LayoutParams imageParams = mCallBack.getHeaderLayout().getLayoutParams();
int targetHeight = imageNewHeight + (int) (event.getY() - startY);
if (targetHeight >= mHeadImageViewMaxHeight) {
listParams.height = mHeadImageViewMaxHeight;
imageParams.height = mHeadImageViewMaxHeight;
} else if (targetHeight >= mHeadImageViewHeight) {
listParams.height = targetHeight;
imageParams.height = targetHeight;
} else {
listParams.height = mHeadImageViewHeight;
imageParams.height = mHeadImageViewHeight;
}
view.getChildAt(0).setLayoutParams(listParams);
mCallBack.getHeaderLayout().setLayoutParams(imageParams);
}

private boolean currentListHeaderViewNotNull() {
return mCallBack.getCurrentPageListHeaderView() != null;
}

private void resetHeaderLayout(View listHeader, View headerLayout) {
ResetAnimimation animation = new ResetAnimimation(
listHeader, mHeadImageViewHeight);
animation.setDuration(ANIM_DELAY_TIME);
listHeader.startAnimation(animation);
ResetAnimimation headAnimation = new ResetAnimimation(
headerLayout, mHeadImageViewHeight);
headAnimation.setDuration(ANIM_DELAY_TIME);
headerLayout.startAnimation(headAnimation);
}

/**
* 回调接口
*/
public interface CallBack {
/**
* 获取含头部图片的布局
* @return layout
*/
public RelativeLayout getHeaderLayout();

/**
* 获取歌手简介的按钮
* @return Button
*/
public LinearLayout getIntroductionLayout();

/**
* 获取头部图片阴影
* @return View
*/
public View getHeaderShadow();
/**
* 获取标签背景View
* @return View
*/
public View getTabBgView();

/**
* 获取当前标签页列表
* @return ListView
*/
public ListView getCurrentPageListView();

/**
* 获取当前标签页列表头部
* @return View
*/
public View getCurrentPageListHeaderView();

/**
* 更新其它页的ListView
*/
public void updateOtherPageListView();
/**
* 请求下一页
* @param view AbsListView
* @param firstVisibleItem int
* @param visibleItemCount int
* @param totalItemCount int
*/
public void requestNextPage(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount);
}

/**
* 下拉回弹的动画
*/
public class ResetAnimimation extends Animation {
private int mTargetHeight;
private int mOriginalHeight;
private int mExtraHeight;
private View mView;

protected ResetAnimimation(View view, int targetHeight) {
this.mView = view;
this.mTargetHeight = targetHeight;
mOriginalHeight = view.getHeight();
mExtraHeight = this.mTargetHeight - mOriginalHeight;
}

@Override
protected void applyTransformation(float interpolatedTime,
Transformation t) {
int newHeight;
newHeight = (int) (mTargetHeight - mExtraHeight * (1 - interpolatedTime));
mView.getLayoutParams().height = newHeight;
mView.requestLayout();
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  技术 标签置顶