先看效果图。
最近boss要在项目里面实现一个顶部悬浮的效果,在网上找了不少项目,基本上有三种方案:
- 1.整个布局分为上下两层,下面那层是有listview的布局,上面那层是悬浮view,而且固定在底部;一开始悬浮的view隐藏,通过监听listview的滑动状态来控制那个悬浮view 的显隐来达到“悬浮”的效果。(示例代码)
- 2.这种也是分两层,下面还是listview的布局,上面也是一个viewgroup(LinearLayout、RelativeLayout均可),不同的是,第一种方案中悬浮的那个view实际上是有两个的,毕竟隐藏的那个悬浮view显示出来之后,下面那层的跟悬浮view相同外观的view依然会随着listview 的滚动而继续滚出屏幕,只是用户看不见了而已。
而这一种方案下的悬浮view只有一个,第一种方案是通过setVisibility控制显隐来实现“悬浮”的效果,这一种是通过addView、removeView来实现,就是当需要显示悬浮view 的时候,将悬浮view从底部那层布局中“抠”出来,添加到上面那层固定在顶部的viewgroup中;当需要隐藏悬浮view的时候,将它从上层固定的viewgroup中“抠”出来,添加到底层布局中。(示例代码)- 3.github上也有人封装好了框架,没仔细研究,有兴趣可以关注下。(示例代码)
另外需要说明的是,上面三种方案都是需要将布局嵌套在scrollview的,因为只有这样才能将布局整体向上滚。
scrollview嵌套listview本身有多麻烦我就不多提了,尤其是listview的长度计算的问题,网上也有几种方案,比如手动计算每个item的长度,然后加起来;还有一种是重写listview的onMeasure方法,然后将它的高度设置成无限长。
这几种方法对于简单的item还可以,但是复杂的自定义view这些高度计算的时候却不是很准确,而且这些方案的滚动都是基于scrollview的滚动,相当于将listview变成了一个非常长的垂直liearlayout。
基于以上的种种原因,所以有了下面从事件分发的角度来处理的这种方案。这种方法外部调用很简单,只需要传一个需要显隐的view就可以,布局中也不需要嵌套scrollview,自然少了很多麻烦。当然仁者见仁智者见智,大家根据实际项目使用适合自己的方案就好。
总体思路就是,当listview的第一个item可见时,要判断是不是需要拦截掉触摸事件,如果需要拦截,则对header的显隐进行操作,如果不需要拦截,则将触摸事件直接交由listview处理。
package com.passerby.pinnedlistview;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.AbsListView;
import android.widget.LinearLayout;
import android.widget.ListView;
/** * Created by mac on 16/1/4. */
public class PinnedListView extends ListView implements AbsListView.OnScrollListener {
public static final int FAULT_TOLERANCE = 3;
private int mHeaderHeight;
private int mThreshold;
private View mHeaderLayout;
private int mStartY;
private boolean mFirstItemIsVisible;
private boolean mShouldInterruptEvent = false;
public PinnedListView(Context context) {
this(context, null);
}
public PinnedListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PinnedListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOnScrollListener(this);
}
// 移动超过 mHeaderHeight的三分之一时,收起header
// 反之,执行正常的操作
@Override
public boolean onTouchEvent(MotionEvent ev) {
final MotionEvent event = ev;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mShouldInterruptEvent = false;
mStartY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
float offsetY = (int) (mStartY - event.getY());
if (shouldInterruptEvent(offsetY)) {
mShouldInterruptEvent = true;
}
if (isListViewOnTop() && hideHeaderLayout(offsetY)) {
// v(true + "");
mShouldInterruptEvent = true;
}
if (mShouldInterruptEvent) {
clearFocus();
return true;
}
mStartY = (int) event.getY();// 每次记录上一次的触摸位置,避免用户手指改变方向时导致判断出错
break;
case MotionEvent.ACTION_UP:
if (mShouldInterruptEvent) {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mHeaderLayout.getLayoutParams();
final int currentTopMargin = params.topMargin;
if (currentTopMargin < -mThreshold) {
// 如果上滑 没有 超过临界值,则强制展开header
params.topMargin = -mHeaderHeight;
} else {
// 如果上滑超过了临界值,则强制收起header
params.topMargin = 0;
}
mHeaderLayout.requestLayout();
// 避免持续保留焦点,否则子view可能保持着触摸时的外观
clearFocus();
return true;
}
break;
}
return super.onTouchEvent(ev);
}
/** * listview在顶部时,继续下拉的事件将要被拦截,因为有的手机(比如vivo)有回弹效果(listview处在顶部时可以继续下拉) */
private boolean shouldInterruptEvent(float offset) {
View child = getChildAt(0);
if (null == child) {
return true;
}
final float tOffset = offset;
//listview滑动到顶的时候下滑时,拦截事件
if (tOffset < -FAULT_TOLERANCE && isListViewOnTop()) {
return true;
}
return false;
}
private boolean isListViewOnTop() {
// v("childTop= " + getChildAt(0).getTop());
// 这里有两个判断,原因如下:
// 1.OnScrollListener里面可以获取当前显示的第一个可见的item,
// 但是从下往上快滚动firstVisibleItem变成0的时候,此时第0个item并没有完全显示
// ,但是如果我们直接让他执行展开header的操作,对用户来说这种显示效果可能并不是他们想要的
// 2.listview有复用机制,直接getChildAt(0)是无法判断是否已经滚动到顶部的
return mFirstItemIsVisible && getChildAt(0).getTop() == 0;
}
private boolean hideHeaderLayout(final float offset) {
final int headerHeight = mHeaderHeight;
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mHeaderLayout.getLayoutParams();
int currentTopMargin = params.topMargin;
float topMargin = currentTopMargin;
float tOffset = offset;
if (tOffset > FAULT_TOLERANCE && currentTopMargin >= -headerHeight) { // 如果往上滚
topMargin = currentTopMargin - tOffset;
// 处理滚动过度的情况
if (topMargin < -headerHeight) {
topMargin = -headerHeight;
}
} else if (tOffset < -FAULT_TOLERANCE && currentTopMargin <= 0) { // 如果往下滚
topMargin = currentTopMargin - tOffset;
// 处理滚动过度的情况
if (topMargin > 0) {
topMargin = 0;
}
}
// mHeaderLayout.getLayoutParams().topMargin = (int) topMargin;
params.topMargin = (int) topMargin;
mHeaderLayout.requestLayout();
// 如果高度有所改变,说明该滑动事件已经被拦截了
return topMargin != currentTopMargin;
}
public void setHeaderLayout(View view) {
this.mHeaderLayout = view;
this.mHeaderLayout.getViewTreeObserver()
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mHeaderHeight = mHeaderLayout.getHeight();
mThreshold = mHeaderHeight >> 1;
mHeaderLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
public void v(String msg) {
if (!TextUtils.isEmpty(msg)) {
Log.v(getClass().getCanonicalName(), msg);
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
mFirstItemIsVisible = firstVisibleItem == 0;
}
}
注释都写得很清楚了,就不多提了。
下载源码