热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

android之滑动悬浮tab&无限循环的viewPager

效果图如下:虽然listview现在已经过时,而且这种效果也满地都是,但是因为自己项目的原因还是自己写一个,而且也想整合都涉及的优化知识点,所以还是值得写一写,当作练练手,也算是一种提升

效果图如下:



虽然listview现在已经过时,而且这种效果也满地都是,但是因为自己项目的原因还是自己写一个,而且也想整合都涉及的优化知识点,所以还是值得写一写,当作练练手,也算是一种提升吧

一:知识点

     1、属性动画的实现view的移动,让其悬浮在顶部      2、HorizontalScrollview计算宽度实现选中tab居中      3、Fragment避免预加载      4、viewPager实现真正的无限循环只需要5个fragment(思路及原理网上是有的),而不是通过设置viewPager的无限大来实现      5、Fragment中的listview和滑动时的事件冲突解决(外部拦截即父类拦截)
其中知识点3、4不进行讲解 知识点3可以移步我的另一篇博客: http://blog.csdn.net/zhongwn/article/details/50382135
知识点4:可以查看这个作者的博客 http://blog.csdn.net/oweixiao123/article/details/23459041

二、原理

原理的话一步步拆分就不是那么的难了,一下逐一分析

   1、悬浮tab

         (1)悬浮的tab是一个horizontalScrollview,重写FrameLayout为SlideRootFrameLayout作为activity的布局中父布              局,tab自然是它的一个子view,所以我们可以在这里搞事情,重写这个主要是滑动事件用到

       (2)计算tab到SlideRootFrameLayout的距离top,然后通过重写滑动事件,可知其滑动的距离,当手指顺着屏                   幕向上滑动时,tab跟其一起滑动,其实是控制SlideRootFrameLayout滑动,

              《1》若是滑动大于等于top则不再进行滑动

              《2》若是小于top,则向上滑动还是遵循《1》,向下滑动则就是要恢复到原来的位置,由于滑动的时候可                          知道其滑动的偏移量,所以向下滑动时,滑动距离超过这个偏移量则将偏移量置0就回到原来位置

        注意:这里所说的向上向下滑动,都是手指顺着屏幕操作,即手指向上滑动或手指向下滑动


代码如下:

重写父布局SlideRootFramelayout的onTouchEvent如下

 @Override
public boolean onTouchEvent(MotionEvent ev) {
if (mTouchInterceptionListener != null) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInitialPoint = new PointF(ev.getX(), ev.getY());
MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
event.setLocation(ev.getX(), ev.getY());
mTouchInterceptionListener.onDownMotionEvent(event);
break;
case MotionEvent.ACTION_MOVE:
float diffX = ev.getX() - mInitialPoint.x;
float diffY = ev.getY() - mInitialPoint.y;
mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_CANCEL:
mBeganFromDownMotiOnEvent= false;
mTouchInterceptionListener.onUpOrCancelMotionEvent(ev,mIntercepting);

// Children's touches should be canceled regardless of
// whether or not this layout intercepted the consecutive motion events.
/*if (!mChildrenEventsCanceled) {
mChildrenEventsCanceled = true;
if (mDownMotionEventPended) {
mDownMotiOnEventPended= false;
MotionEvent event1 = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
event1.setLocation(ev.getX(), ev.getY());
duplicateTouchEventForChildren(ev, event1);
} else {
duplicateTouchEventForChildren(ev);
}
}*/
break;
}
return true;
}
return super.onTouchEvent(ev);
}
主要是在Action_Move中搞事情:这里为了更好的扩展自定义一个接口
 mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);
将移动的偏移量返回,再来看看具体实现
  @Override            public void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY) {               /* ViewDragHelper.create(slideRootFrameLayout, new ViewDragHelper.Callback() {                    @Override                    public boolean tryCaptureView(View child, int pointerId) {                        return false;                    }                })*/                doMoveHeadFloatTab(diffX, diffY);            }
 /**     * 处理当滑动时,悬浮的tab     *     * @param diffX     * @param diffY     */    private void doMoveHeadFloatTab(float diffX, float diffY) {        //最大只能移动的距离是 llHeadParent.getHeight()        float currTranstiOnY= ViewHelper.getTranslationY(slideRootFrameLayout);        float translatiOnY= getNegativeMaxValue(currTranstionY + diffY, -llHeadParent.getHeight(), 0);        if (translationY <= 0 && translationY != currTranstionY) {//手指向上滑动,并且没有滑动到顶部            ViewHelper.setTranslationY(slideRootFrameLayout, translationY);            //移动多上距离这个布局就要增加多少布局,否则会显示不全,底部会留有一处空白            FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) slideRootFrameLayout.getLayoutParams();            //一定要减去titleBar,如果没有去掉Winow.xxx.Title,还要减去这个高度,否则会显示不全            lp.height = (int) (-translationY + Tools.getScreenSize(context).y - Tools.getStatusBarHeight(context));            slideRootFrameLayout.requestLayout();//请求重绘,但是会有一闪一闪的情况        }    }
主要逻辑是在这个方法:

ViewHelper这个是一个工具包,其实里边就是属性动画的库,直接使用就好了

/***
* 手指上移过程dy是负数
* 返回负数最大值:0是最大值,不可以超过
*
* @param value 移动的最终距离:上次的位置+当次移动的偏移量之和,就是本次要移动的最终的偏移量
* @param canMoveMaxValue 可移动的最大值
* @param maxValue
* @return
*/
public static float getNegativeMaxValue(final float value, final float canMoveMaxValue, final float maxValue) {
return Math.min(maxValue, Math.max(canMoveMaxValue, value));
}
这个方法是获取滑动时的距离,向上滑动时dy是负数所以这里比较最大值设置0
得到滑动的距离之后,接下来就是移动SlideRootFramelayout,其直接借助viewHelper.setTranslationY搞事情就行,

注意:

//一定要减去titleBar,如果没有去掉Winow.xxx.Title,还要减去这个高度,否则会显示不全
lp.height = (int) (-translationY + Tools.getScreenSize(context).y - Tools.getStatusBarHeight(context));
slideRootFrameLayout.requestLayout();//请求重绘,但是会有一闪一闪的情况

SlideRootFramelayout布局向上移动多少就要增加多少高度,否则会显示不全,而且一定要重绘,否则不会更新

这样就实现了悬浮的tab啦,是不是很简单

  

    2、HorizontalScrollView中的tab居中

         (1)、我的思路是将屏幕宽分为三分,即只显示3个view

         (2)、当滑动viewpager或者选中当前的view时,通过获取当前的view距离horizontalScrollview的距离,然后往左滑动一个view的宽度,选中的view就居中了

代码如下:

private void init() {
screenWidthOneThird= Tools.getScreenSize(context).x / 3;
tabTextViewList = new ArrayList();
}
将屏幕分为三份

然后根据tab数据源生成N个tabView

 /**
* @description 添加tab栏:资源集合
* @author zhongwr
* @update 2015年9月1日 下午5:24:44
*/
@SuppressLint("ResourceAsColor")
public void addTabList(ArrayList allTabList) {
if (!Tools.isListEmpty(allTabList)) {
this.allTabList = allTabList;
llTabContainer.setVisibility(View.VISIBLE);
llTabContainer.removeAllViews();
int size = allTabList.size();
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.leftMargin = 30;
layoutParams.rightMargin = 30;
layoutParams.gravity = Gravity.CENTER_VERTICAL;
layoutParams.width = screenWidthOneThird - 60;// 左右两边间距
for (int i = 0; i TabItem tabItem = allTabList.get(i);
TextView tvTab = createTabTextView(tabItem, layoutParams);
tvTab.setOnClickListener(new TabOnClickListener(tabItem.tabIndex));
tabTextViewList.add(tvTab);
if (1 == tabItem.selected) {// 当前选中的
currTabIndex = tabItem.tabIndex;
tvCurrTab = tvTab;
tvTab.setTextColor(context.getResources().getColor(R.color.red1));
} else {
tvTab.setTextColor(context.getResources().getColor(R.color.gray2));
}
llTabContainer.addView(tvTab);
// 增加竖线
View line = new View(context);
line.setBackgroundColor(context.getResources().getColor(R.color.color_line_e2));
LinearLayout.LayoutParams layoutline = new LinearLayout.LayoutParams(1, 30);
line.setLayoutParams(layoutline);
llTabContainer.addView(line);
}
if (null != onClickTabListener) {
onClickTabListener.onDefualtTab(currTabIndex, allTabList.get(currTabIndex));
}
scrollToPosition(currTabIndex);
} else {
llTabContainer.setVisibility(View.GONE);
}
}
这里是通过动态加载的tabView,llTabContainer是HorizontalScrollview的子view是tabView的父类容器

/**
* @description 设置定位到指定的位置,左右滑动都是往左滑动一个view的宽度,选中的view就居中了
* @author zhongwr
* @update 2015-11-30 下午3:53:31
*/
public void scrollToPosition(final int currTabIndex) {
scrollView.post(new Runnable() {
@Override
public void run() {// 选中的view居中
TextView textView = tabTextViewList.get(currTabIndex);
int left = textView.getLeft();
left = left - screenWidthOneThird;
scrollView.scrollTo(left, 0);
}
});
}
不管是点击左边还是右边的tabView,都是按照向左边滑动一个tabView的宽度,让选中的tabView居中。

主要代码就是这样,是不是觉得难度其实也没什么,就是靠思路及计算

这些前期工作都已经搞完,解决滑动冲突才真正是个难点

    3、解决滑动悬浮tab和viewpager中的listView的冲突

              解决事件冲突的方式无非就是两种:           (1)、外部拦截法:父类控制是否要拦截事件,                   重写拦截方法onInterceptTouchEvent() 返回true 拦截事件  false:不拦截           (2)、内部拦截法:子类通知父类是否需要拦截,                  requestDisallowInterceptToucheEvent(boolean)  false:拦截  true :不拦截

              基于上边两个方法规则,这里我选用第一种方法:外部拦截法

         解决冲突还是要一步步分析,什么时候拦截,什么时候不拦截?

         《1》当向上滑动的时:

                  1、刚进到页面还没滑动,则直接拦截

                  2、已滑动,但是tab还没置顶悬浮,则直接拦截,所以1和2可以合起来,tab还没置顶悬浮直接拦截

                  3、当tab已悬浮,则不再进行拦截,把事件交给子view(这里是交给listview)

        《2》当向下滑动时:

                 1、当tab悬浮时:

                       <1> listview已经滑动,则不拦截,让listview回到初始位置:即position = 0;

                      <2> listview已经在初始位置(回到初始位置或者不曾滑动过)则,直接通知父类拦截事件

                 2、当tab未悬浮时:

                       <1> 刚进入,tab还是初始位置,则不拦截,将事件交给子view(listview)可以滑动

                       <2>已滑动,但并未置顶悬浮,只是滑动到一半,则直接拦截,让tab回到初始位置

     基本就是这样,分析完成之后,接下来就是直接撸码了。

SlideRootFrameLayout:在外部拦截,这都是交给自定义的接口实现

  @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mTouchInterceptiOnListener== null) {
return false;
}


switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInitialPoint = new PointF(ev.getX(), ev.getY());
mPendingDownMotiOnEvent= MotionEvent.obtainNoHistory(ev);
mDownMotiOnEventPended= true;
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, false, 0, 0);
mBeganFromDownMotiOnEvent= mIntercepting;
mChildrenEventsCanceled = false;
return mIntercepting;
case MotionEvent.ACTION_MOVE:
// ACTION_MOVE will be passed suddenly, so initialize to avoid exception.
if (mInitialPoint == null) {
mInitialPoint = new PointF(ev.getX(), ev.getY());
}

// diffX and diffY are the origin of the motion, and should be difference
// from the position of the ACTION_DOWN event occurred.
float diffX = ev.getX() - mInitialPoint.x;
float diffY = ev.getY() - mInitialPoint.y;
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);
return mIntercepting;
}
return false;
}
自定义的接口实现
TouchInterceptionListener.shouldInterceptTouchEvent():

 @Override
public boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY) {
return doInterceptEvent(diffX, diffY);

}

所有的处理都交给了doInterceptEvent():

/**
* 处理拦截事件
*
* @param diffX
* @param diffY
* @return
*/
private boolean doInterceptEvent(float diffX, float diffY) {
float currTranstiOnY= ViewHelper.getTranslationY(slideRootFrameLayout);
float headHeight = -llHeadParent.getHeight();
if (Math.abs(diffY) > Math.abs(diffX)) {//上下滑动
if (diffY <0) {//手指向上滑动
if (Math.abs(currTranstionY) >= Math.abs(headHeight)) {//移动到顶端(tab悬浮)
isUpInterception = false;
isTabFloat = true;
} else {//还没移动到顶部所以还是要拦截
isUpInterception = true;
isTabFloat = false;
}
// return isUpInterception;
} else if (diffY > 0) {//手指向下滑动
if (isTabFloat) {//如果tab悬浮着,手指要向下滑动,要拦截将tab复原
if (!viewPagerManager.getCurrentFragment().isFragmentViewIntercepted()) {//listview已经滑动了
isUpInterception = false;
} else {
isUpInterception = true;
if (Math.abs(currTranstionY) <= 0) {//向下滑动复原
isTabFloat = false;
}
}
} else {//tab未悬浮,两种可能性:一个是可能刚进入时手指向下滑动时不拦截,一个是滑动到一半时要拦截
if (Math.abs(currTranstionY) <= 0) {//刚进入时,手指向下滑动,不拦截
isUpInterception = false;
} else if (Math.abs(currTranstionY) isUpInterception = true;
}
}
// return isUpInterception;

}
return isUpInterception;
} else {//左右滑动不拦截
return false;
}
}
以上的处理逻辑就是跟我之前分析的一样,这里需要还有一处地方就是,listview是否已经滑动了或者是否已经回到初始位置了,需要获取或者释放事件主动权要告知父类,当然也是要自定义实现的,这里只对外部提供一个方法:

viewPagerManager.getCurrentFragment().isFragmentViewIntercepted()
这方法就是通知外部是否需要拦截事件;

由于tab未置顶悬浮或不在初始位置时,listview是不可以滑动的,所以只有在tab置顶浮或回到初始位置时,才可以滑动,才有获取或者释放事件的主动权;

接下来分析在什么情况下,listview需要掌握主动权:

(1)、当向上滑动的时,外部会在之前的规则不拦截事件,此时listview可以任意向上滑动,这种情况可以不管

(2)、当向下滑动时,要回到初始位置,既是第一个位置 position=0;因为只有到了初始位置才通知外部拦截事件,否则不可以拦截事件。

滑动的话,我们立即想到的就是ScrollListener

public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

if (0 == firstVisibleItem) {
if (getChildCount() > 0) {
View firstView = getChildAt(0);
if (0 == firstView.getTop()) {
isViewIntercepted = true;
}else{
isViewIntercepted = false;
}

}
} else {
isViewIntercepted = false;
}
}

onScroll方法:因为它可以直接获取到一个可见view的position,所以当时position=0时,可以通知拦截;但是这里直接拦截会有bug,因为会出现firstView没显示全就被拦截;所以这里拿到firstView.getTop();这个top值如果不是0则表示没显示全,则不拦截,显示全则通知拦截;主要是isViewIntercepted这个标志了,以下是对外的方法,外部通过Fragment间接调用的;


/**
* 当前类是否要被拦截
*/
private boolean isViewIntercepted = false;

/**
* 当前view是否被拦截
*/
public boolean isViewIntercepted() {
return isViewIntercepted;
}


这篇文章讲的这里算是结束了。

说说这里遇到的最大的坑

这里设计的知识点以及坑尤其是ViewPager的无限循环使用Fragment会有许多坑;比如循环使用更新数据、缓存数据、listview定位的缓存此外最坑的是 onSelectedPage执行时Fragment并没有完全绑定activity,这时就要考虑什么时间点去更新数据,因为没绑定时可能会出现getActivity为null等等问题,所以如果不是特别大的话加载量的话,不建议使用无限循环的Fragment,以上的缓存数据也很难管理,此外选中tabView时的定位,要对应上的页数也需要很大的功夫,所以还是建议使用老套方法,有多少个tab就创建多少个Fragment,只要控制懒加载就好了,其它都很好管理,毕竟那么点东西android的内存还是妥妥的,而且一般用户都有自己喜欢的某个tab,用户很少去把所有的tab都点了个遍。不使用无限循环可以通过这个demo去改造就好了,改起来应该比较好改。


demo如下:http://download.csdn.net/detail/zhongwn/9732910


推荐阅读
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • SmartRefreshLayout自定义头部刷新和底部加载
    1.添加依赖implementation‘com.scwang.smartrefresh:SmartRefreshLayout:1.0.3’implementation‘com.s ... [详细]
  • Android开发实现的计时器功能示例
    本文分享了Android开发实现的计时器功能示例,包括效果图、布局和按钮的使用。通过使用Chronometer控件,可以实现计时器功能。该示例适用于Android平台,供开发者参考。 ... [详细]
  • 本文介绍了一款名为TimeSelector的Android日期时间选择器,采用了Material Design风格,可以在Android Studio中通过gradle添加依赖来使用,也可以在Eclipse中下载源码使用。文章详细介绍了TimeSelector的构造方法和参数说明,以及如何使用回调函数来处理选取时间后的操作。同时还提供了示例代码和可选的起始时间和结束时间设置。 ... [详细]
  • 本文详细介绍了Android中的坐标系以及与View相关的方法。首先介绍了Android坐标系和视图坐标系的概念,并通过图示进行了解释。接着提到了View的大小可以超过手机屏幕,并且只有在手机屏幕内才能看到。最后,作者表示将在后续文章中继续探讨与View相关的内容。 ... [详细]
  • 带添加按钮的GridView,item的删除事件
    先上图片效果;gridView无数据时显示添加按钮,有数据时,第一格显示添加按钮,后面显示数据:布局文件:addr_manage.xml<?xmlve ... [详细]
  • 1简介本文结合数字信号处理课程和Matlab程序设计课程的相关知识,给出了基于Matlab的音乐播放器的总体设计方案,介绍了播放器主要模块的功能,设计与实现方法.我们将该设 ... [详细]
  • 开发笔记:图像识别基于主成分分析算法实现人脸二维码识别
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了图像识别基于主成分分析算法实现人脸二维码识别相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • 深入理解CSS中的margin属性及其应用场景
    本文主要介绍了CSS中的margin属性及其应用场景,包括垂直外边距合并、padding的使用时机、行内替换元素与费替换元素的区别、margin的基线、盒子的物理大小、显示大小、逻辑大小等知识点。通过深入理解这些概念,读者可以更好地掌握margin的用法和原理。同时,文中提供了一些相关的文档和规范供读者参考。 ... [详细]
  • 在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板
    本文介绍了在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板的方法和步骤,包括将ResourceDictionary添加到页面中以及在ResourceDictionary中实现模板的构建。通过本文的阅读,读者可以了解到在Xamarin XAML语言中构建控件模板的具体操作步骤和语法形式。 ... [详细]
  • 本文介绍了绕过WAF的XSS检测机制的方法,包括确定payload结构、测试和混淆。同时提出了一种构建XSS payload的方法,该payload与安全机制使用的正则表达式不匹配。通过清理用户输入、转义输出、使用文档对象模型(DOM)接收器和源、实施适当的跨域资源共享(CORS)策略和其他安全策略,可以有效阻止XSS漏洞。但是,WAF或自定义过滤器仍然被广泛使用来增加安全性。本文的方法可以绕过这种安全机制,构建与正则表达式不匹配的XSS payload。 ... [详细]
  • 微信小程序导航跟随的实现方法
    本文介绍了在微信小程序中实现导航跟随的方法。通过设置导航的position属性和绑定滚动事件,可以实现页面向下滚动到导航位置时,导航固定在页面最上方;页面向上滚动到导航位置时,导航恢复到原始位置;点击导航可以平滑跳转到相应位置。代码示例也给出了具体实现方法。 ... [详细]
  • 本文总结了在编写JS代码时,不同浏览器间的兼容性差异,并提供了相应的解决方法。其中包括阻止默认事件的代码示例和猎取兄弟节点的函数。这些方法可以帮助开发者在不同浏览器上实现一致的功能。 ... [详细]
author-avatar
守护琳的心
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有