基于事件分发机制,以最小代价实现listview顶部悬浮效果
2016-01-05 16:34
513 查看
先看效果图。
最近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处理。
注释都写得很清楚了,就不多提了。
下载源码
最近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处理。
[code]public class PinnedListView extends ListView implements AbsListView.OnScrollListener { /** * 容错值 */ private 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()); // v(String.format("startY=%s,endY=%s,offsetY=%s", mStartY, event.getY(), offsetY)); if (shouldInterruptEvent(offsetY)) { mShouldInterruptEvent = true; } if (isListViewOnTop() && changedHeaderLayout(offsetY)) { mShouldInterruptEvent = true; } if (mShouldInterruptEvent) { return true; } mStartY = (int) event.getY();//每次记录上一次的触摸位置,避免用户手指改变方向时导致判断出错 break; case MotionEvent.ACTION_UP: if (mShouldInterruptEvent) { final int currentHeight = mHeaderLayout.getLayoutParams().height; if (mHeaderHeight - currentHeight < mThreshold) { //如果上滑 没有 超过临界值,则强制展开header mHeaderLayout.getLayoutParams().height = mHeaderHeight; } else { //如果上滑超过了临界值,则强制收起header mHeaderLayout.getLayoutParams().height = 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() { View child = getChildAt(0); if (null == child) { return true; } //这里有两个判断,原因如下: //1.OnScrollListener里面可以获取当前显示的第一个可见的item, // 但是从下往上快滚动firstVisibleItem变成0的时候,此时第0个item并没有完全显示 // ,但是如果我们直接让他执行展开header的操作,对用户来说这种显示效果可能并不是他们想要的 //2.listview有复用机制,直接getChildAt(0)是无法判断是否已经滚动到顶部的 return mFirstItemIsVisible && child.getTop() >= 0; } /** * FAULT_TOLERANCE作为容错值 * 虽然理论上判断tOffset大于或小于0即可,但是手指做点击操作的时候因为实际操作可能并不总是为0 * ,有时候可能有误差,所以这里设置容错值 */ private boolean changedHeaderLayout(final float offset) { final int headerHeight = mHeaderHeight; int currentHeight = mHeaderLayout.getLayoutParams().height; float tOffset = offset; float height = currentHeight; if (tOffset > FAULT_TOLERANCE && currentHeight > 0) { //如果往上滚 //处理滚动过度的情况 int restHeight = currentHeight; if (tOffset > restHeight) { tOffset = restHeight; } height = currentHeight - tOffset; } else if (tOffset < -FAULT_TOLERANCE && currentHeight < headerHeight) { //如果往下滚 //处理滚动过度的情况 tOffset = Math.abs(tOffset); if (tOffset + currentHeight > headerHeight) { tOffset = headerHeight - currentHeight; } height = currentHeight + tOffset; } //如果高度有所改变,说明该滑动事件已经被拦截了 boolean isChanged = height != currentHeight; if (isChanged) { //避免过度刷新 mHeaderLayout.getLayoutParams().height = (int) height; mHeaderLayout.requestLayout();//会引起重绘(onMesuare、onLayout、onDraw) // this.mHeaderLayout.getLayoutParams().height = height; // this.mHeaderLayout.setTranslationY(-height); // mHeaderLayout.setY(-height); // mHeaderLayout.scrollTo(0,height); } return isChanged; } 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); //mHeaderLayout.getLayoutParams().height是-2(wrap_content) //-2的值在程序的逻辑判断中带来太多麻烦,所以设置成准确数值 mHeaderLayout.getLayoutParams().height = mHeaderHeight; } }); } 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; } }
注释都写得很清楚了,就不多提了。
下载源码
相关文章推荐
- 裁剪nutch 8步骤
- 微信支付(APP)
- 关键字-const
- 九十分钟极速入门Linux——Linux Guide for Developments 学习笔记
- meter-察看结果树-响应数据,中文显示乱码问题处理
- android ProgressDialog 不显示的提示信息的问题
- easyui datagrid 行数据处理
- android三种方式实现自由移动的view
- Apache shiro集群实现 (一) shiro入门介绍
- @Column
- Echarts圆饼状js代码
- Unity Shader 学习笔记(十二) 创建程序纹理贴图
- iOS分享【OC】—— Masonry布局
- ubuntu升级导致virtualbox不能启动问题
- C语言预处理指令
- 输入和输出
- 学习shell script中
- 利用cvGetCols裁剪图像
- JAVA BigDecimal的构造double类型
- MyEclipse8.5配置Maven3.3.9