左右侧滑选项卡的头部悬停,仿微博滚动视图嵌

作者:澳门新葡京平台游戏

在平时项目中,我们经常会碰到类似UIScrollView嵌套UITableView的需求,虽然苹果并不建议这种做法,但是这样的需求在平时工作中还是很常见的。比如笔者公司项目之前用到的地方:

PS:也是看别人写的。自己刚好有这个需求,看了代码,改吧改吧就用了。找不到那篇文章了。

前言

由于业务需要做成类似腾讯课堂课程详情滚动的效果,考虑到后面有可能有新的呈现方式,RN提供的组件没有这种滚动控件,不如自己封装。

图片 1

效果图.gif

  • 项目地址 在这里,如果有好的意见欢迎提 issue或pr。

现在上市场上很多主流的App模块都是很庞大,往往一个页面会有很复杂的功能,但是再大的模块都是由一个个简单的模块堆叠起来的。模块多了之后,有时要处理好模块之间的联系不是那种想都不用想就可以开干的。还涉及到不同模块之间的手势和动画处理。

图片 2FSScrollViewNestTableViewDemo.gif

效果:实现tableview的嵌套,页面可以左右滑动切换页面。头部轮播图隐藏的时候,选项卡实现头部悬停的效果。如下:

开始

  • 我们先来看下,腾讯课堂视频播放详情页面是怎么样的?

图片 3

腾讯课堂视频.gif

  • 咋一看界面感觉有点复杂,其实简化来说,这个界面可以看成tab组件 scroll组件。哲学上说,要抓好主要矛盾与次要矛盾,这个问题的主要矛盾是scroll组件实现,也就是最外层的RNFixScrollView。

图片 4

分解图.png

  • 说道这里,我尝试着写了个测试js例子,最外面套一个ReactNative自带的ScrollView并设置视频播放控件的高度为200Tab导航控件style={{height: windowHeight- 80}},那这样滚动距离到120时,滚动条到底部了,视频播发控件的区域距离屏幕顶部还有80。

  • 跑起来运行后发现的一个严重的问题是,如果Tab导航控件的内容区域存在ScrollView或者ListView时,无法滚动,只有最外层可以滚动,也就是手势滚动被拦截了?

  • 一开始想两种大的思路:一种是完全靠JS层面,通过ScrollView暴露的API去实现,第二种是原生 JS,这里涉及到几个关键的东西,如何寻找Tab导航控件中的ScrollView或者ListView和控制手势实现的效果

    外层滚动容器到顶部 手势往上则通知内层滚动容器开始滚动;内层到顶部 手势往下则通知外层开始滚动。

  • 发现第一种方法在解决如何寻找子控件并判断滚动状态上没有方法(可能是我没发现)以及性能上的考量,那就采用第二种方法。

为了举个比较好的栗子,就拿微博个人信息界面动动手。通过观察微博个人信息界面,总结出需要注意的三个地方:

可以看到最上面是一块轮播图组件,我也已经整理了一份轮播图组件在github上面,轮播图下面还有若干条其他的内容,而且这几块内容都是后台动态控制显示的,所以这里用UIScrollView设置顶部偏移量实现是很不方便的。最外层我肯定选择使用tableView分区管理来实现。而下面的横向分类页面肯定需要tableView去实现

图片 5

分析

为了解决上面的问题,我们需要了解几个关键点。

  • 一个是怎么判断手势滑动以及外层滚动容器到底部和内层滚动到顶部?
  • 第二个是寻找滚动组件并通知内层滚动组件开始滚动?

因此,网上搜寻这两个问题的相关资料和解决办法,判断是否到底部很容易搜到了,当然了解了其原理。另外,判断手势是往上滑还是往下滑的问题放到后面说明。

寻找内层滚动容器,一开始是认为递归寻找可见的ScrollView实例(Android中界面控件是一种树形结构),通过Hierarchy Viewer工具发现这三个都是可见的,随后对比三个ScrollView属性发现其在屏幕上的LocationOnScreenX坐标不同,如果当前滚动容器显示则等于0。

剩下最后一个如何通知内层容器滚动呢?先卖个关子,在解决这个问题之前,我们先来了解下Android中的View事件是如何传递的。

正所谓知己知彼,百战不殆,看看Android触摸事件类型有哪些?我们想下玩手机的时候手指的情况:落下手指,抬起手指,移动手指是三种基本的操作,其实也是3种触摸事件,分别代表着MotionEvent.ACTION_DOWN,MotionEvent.ACTION_UP,MotionEvent.ACTION_MOVE

简单来说,如下图所示:触摸事件发生后,如果事件的坐标处于ViewGroup的管辖范围,那么首先调用ViewGroup的dispatchTouchEvent方法,然后其内部调用onInterceptTouchEvent()方法来判断是否拦截该触摸事件,若拦截该事件则调用ViewGroup的onTouchEvent()方法,否则的话,交给其子View的dispatchTouchEvent处理。

图片 6

image.png


具体可以参考我以前写的事件分发机制学习。

回过头来讲外层滚动容器通知内层滚动,其实通知滚动相当于不拦截事件,那么就是重写 onInterceptTouchEvent方法并返回false。而这个方法会随着手势不断调用,这时候聪明的你想到了啥?根据手触摸屏幕的y坐标差来判断手势往上还是往下。手指滑动时会产生一系列触摸事件,这里有两种情况:说明下屏幕的左上角是坐标原点,沿着右边是x轴,左边则是y轴。
① Down -> Move ... -> Move -> UP
② Down -> Move ->... -> Move

记录Down触摸事件的Y坐标值作为起始值,Move或者UP的Y坐标值作为末尾值,两者之差大于最小滑动值则说明向上滑,小于最小滑动值则说明向上滑(这里简化了条件,如果是实现OnGestureListener的话判断滑动的话还有X轴滑动速度值和Y轴滑动速度值)。到这里前面提的两个问题都得到解决了,下面开始真正上手了。

  1. 头部背景图滚动处理
  2. 两层滚动视图嵌套手势处理
  3. 按钮底部毛毛虫动画效果

这样的话问题就来了,先不说下面的横向分页如何实现,这个页面必定需要在一个tableView中嵌套多个不同的tableView,但是如果嵌套了其他的scrollView,就需要我们判断不同条件下让哪个scrollView响应滑动事件,因此这里我们需要解决的就是手势的冲突问题

滑动效果.gif

如何封装RN组件

  • 参考 RN 0.51中文文档,我们需要做这些东西:

头部背景

先看下效果图

图片 7scroll.gif首先第一步分析 顶部的ImageView,如果手势向上滚动我们会发现它就和 TableHeaderView差不多,当向下拉伸直到TableView出现弹性效果的时候,头部的ImageView变高了,但是不是放大。而头像是保持不动的。因此,顶部ImageView不是TableHeaderView,它的坐标会随着TableView滚动作出不同的改变。它的位置是在TableView下面,设置tableHeaderView为透明即可。在scrollViewDidScroll:代理函数中实现(-60是它的初始y坐标)

 if(offsetY <= 0){ self.coverView.y = -offsetY/2   ; }else{ self.coverView.y = -offsetY   ; }

最开始想到的是在滑动tableView时候根据偏移量设置scrollEnabled来控制两个tableView的响应,但是会有个问题就是当滚动到顶部时用户必须第二次滑动才可以显示出外层的tableView,这样就会造成一个卡顿的效果,所以这个方法是行不通的。

分析过程:
1.看到可以上下滑动,并带有头view,立马想到是最外层是UITableView
2.三个按钮的选项卡可以悬停到头部,立马想到是sectionHeader,tableview用plain样式。更加确定是最外层用tableview做的。
3.可以滑动,这种样式是scrollview或者collectionview上添加tableview

原生上要做的事
  • 1.创建原生固定滚动控件
  • 2.创建管理滚动控件ViewManager的子类
  • 3.创建实现了ReactPackage接口的类

多重嵌套

实现这个布局可以是实现一个TabView,把四个子视图作为TabView的子控件, TabView作为TableView的cell显示出来。但是当我们照做的时候发现,当滚动TableView到底部的时候,TabView的子视图不会滚动,而滚动TabView的子视图到顶部,TableView也不会滚动。之所以会出现这样的情况是因为子控件和父控件手势是单独响应不会传递。解决的方法是自定义一个UITableView重写以下方法

- gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return YES;}

这个方法返回YES可以让子控件接收到父控件的手势事件,子控件的手势也会传给父控件,在这里可以让UITableView的滚动事件传递。

但是实现后发现 滚动四个子控件 父控件也跟着滚动。我们要实现的是:当父控件滚动到底部时子控件才能滚动,子控件滚动到头部时,子控件停止滚动父控件开始滚动。逻辑代码如下:

父控件:

 if(offsetY >= HeaderHeight - 64){ [[NSNotificationCenter defaultCenter] postNotificationName:TabViewScrollToTopNotification object:@]; self.shouldScroll = NO; }else{ [[NSNotificationCenter defaultCenter] postNotificationName:TabViewScrollToTopNotification object:@]; } if(self.shouldScroll == NO){ [scrollView setContentOffset:CGPointMake(0, HeaderHeight - 64)]; }

子控件:

 CGFloat offsetY = scrollView.contentOffset.y; if(offsetY <= 0){ [[NSNotificationCenter defaultCenter] postNotificationName:ItemScrollToTopNotification object:@]; } if(self.shouldScroll == NO){ [scrollView setContentOffset:CGPointZero]; }

下面说一下我的解决思路:这里就直接分为mainTableViewsubTableView,设置mainTableView的联动手势UIGestureRecognizer

所以,层级关系是tableview-->collectionview-->tableview

JavaScript上要做的事
  • 4.实现对应的JavaScript模块

毛毛虫效果

实现毛毛虫动画效果可有多种方式,在这里我用的是CASharpLayer结合UIBezierPath来实现。

仔细观察动画可发现,动画可以切分为两部完成,如下图所示:

图片 8line.png通过改变 CASharpLayerstrokeStartstrokeEnd属性来控制长度即可。

首先, 在初始化布局每个标题按钮的时候,用一个字典把所有按钮下面的坐标记录下来

- setupBtn:(UIButton *)btn index:(NSInteger)index{ if(index == 0){ btn.selected = YES; self.selectBtn = btn; } self.btnDict[@] = btn; self.pointsDict[@] = @{@"start":@(btn.x   2),@"end":@(CGRectGetMaxX(btn.frame)-2)}; btn.tag = index; /** . 其他设置 ...... */}

然后通过监控scrollView的偏移量,结合保存的坐标计算出在不同位置的下标线的strokeStart和strokeEnd

- LHTabViewDidScroll:(LHTabView *)tabView{ CGFloat offsetX = tabView.offset.x; NSInteger index = offsetX/WIDTH; CGFloat zero = [self.pointsDict[@][@"start"] floatValue]; CGFloat currentStart = [self.pointsDict[@][@"start"] floatValue]; CGFloat currentEnd = [self.pointsDict[@][@"end"] floatValue]; CGFloat nextStart = [self.pointsDict[@][@"start"] floatValue]; CGFloat nextEnd = [self.pointsDict[@][@"end"] floatValue]; CGFloat PhysicsDelta = offsetX - index * WIDTH; CGFloat end,start; CGFloat delta = nextEnd - currentEnd; if(PhysicsDelta <= WIDTH/2){ //对应图片 step one delta = (PhysicsDelta/ * delta; end = currentEnd   delta; self.lineLayer.strokeStart = (currentStart - zero)/self.lineTotalWidth; self.lineLayer.strokeEnd = (end - zero)/self.lineTotalWidth; }else{ //对应图片 step two delta = nextStart - currentStart; PhysicsDelta = PhysicsDelta - WIDTH/2; delta = (PhysicsDelta/ * delta; start = currentStart   delta; self.lineLayer.strokeStart = (start - zero)/self.lineTotalWidth; self.lineLayer.strokeEnd = (nextEnd - zero)/self.lineTotalWidth; }}

刚开始的时候发现CAShaplayer滑动的效果和scrollView的滑动有延迟,可以通过CAShaplayer的speed属性来调整动画速率即可 。实现效果图如下:

图片 9animate.gif

以上就是所有的步骤。具体代码可以点击demo

最后吐槽下,因为最近在使用react-native做一个项目,需要实现类似功能,但是因为滑动的效果没有原生的流畅,出现很奇怪的现象,各种不顺,没办法只能放弃使用这种UI结构。看来react-native还有很长的路要走,原生还是第一生产力。

原文链接

/** 同时识别多个手势 @param gestureRecognizer gestureRecognizer description @param otherGestureRecognizer otherGestureRecognizer description @return return value description */- gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{ return YES;}

开始也想着根据偏移量,打开或者关闭用户交互,设置偏移量啥的,试了试,发现效果太差,后来上网搜了,找了别人的方法,看着代码,自己试了试,能达到我需要的效果。自己看明白了,就挪到自己项目里了。

开始动手

下面的分类pageView是直接使用了封装的page框架FSScrollContentView,在mainTableViewUIScrollViewDelegate方法内判断mainTableView的偏移量,当标题栏滚动到顶部的时候固定mainTableView的偏移量

其实全篇看下来,主要是通过一个全局的BOOL值,设置tableview是否需要滑动,在scrollview的代理方法里,根据tableview的偏移量,再结合这个BOOL值,判断是否需要滑动,同时设置tableview的偏移量。

1.创建原生固定滚动控件

根据前面的分析,我们知道写原生滚动控件主要是重写控制拦截事件方法onInterceptTouchEvent,这里先说明下我们只需要判断当前 Tab导航控件 存在 ScrollView 的话才进入我们的逻辑进行拦截控制,否则按默认的逻辑。

  • 需要在 MotionEvent.ACTION_DOWN 事件中,通过前面分析的条件寻找第一个子 ScrollView ,代码如下:
  private ScrollView findScrollView(ViewGroup group) {
        if (group != null) {
            for (int i = 0, j = group.getChildCount(); i < j; i  ) {
                View child = group.getChildAt(i);
                if (child instanceof ScrollView) {
                    //获取view在整个屏幕中的坐标如果x==0的话代表这个scrollview是正在显示
                    int[] location = new int[2];
                    child.getLocationOnScreen(location);
                    System.out.print("locationx:"   location[0]   ",locationy:"   location[1]);
                    if (location[0] == 0)
                        return (ScrollView) child;
                    else
                        continue;

                } else if (child instanceof ViewGroup) {
                    ScrollView result = findScrollView((ViewGroup) child);
                    if (result != null)
                        return result;
                }
            }
        }
        return null;
    }
  • 声明计算滑动手势的两个点 Down点(x1, y1) Move点(x2, y2),这样出现两种情况:向上滑,向下滑

  • 在通过isAtBottom方法,判断RNFixScrollView是否滑到底部。

    public boolean isAtBottom() {
        return getScrollY() == getChildAt(getChildCount() - 1).getBottom()   getPaddingBottom() - getHeight();
    }
  • 综合上面的已知条件,只需要找出几种临界情况:
    RNFixScrolView已到底部&&向上滑:不拦截
    RNFixScrolView未到底部&&向上滑:拦截
    RNFixScrolView未到底部&&向下滑&&子ScrollView已到顶部:拦截
    RNFixScrolView已到底部&&向下滑&&子ScrollView未到顶部:不拦截,代码如下:
   @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!mScrollEnabled) {
            return false;
        }

        int action = ev.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            //当手指按下的时候
            x1 = ev.getX();
            y1 = ev.getY();
            scrollView = findScrollView(this);
            isIntercept = false;
        }

        if ((action == MotionEvent.ACTION_MOVE) || (action == MotionEvent.ACTION_UP)) {
            //Tab导航控件是否存在ScrollView
            if (scrollView != null) {
                //当手指移动或者抬起的时候计算其值
                x2 = ev.getX();
                y2 = ev.getY();
                //判断RNFixScrollView是否到底部 
                isbottom = isAtBottom();
                //向上滑动
                if (y1 - y2 > FLING_MIN_DISTANCE ) {
                    if (!isbottom) {
                        isIntercept = true;
                    } else {
                        isIntercept = false;
                    }
                    return isIntercept;
                } //向下滑动
                else if (y2 - y1 > FLING_MIN_DISTANCE ) {
                    int st = scrollView.getScrollY();
                    if (!isbottom) {
                        isIntercept = true;
                    } else {
                        if (st == 0) {
                            isIntercept = true;
                        } else {
                            isIntercept = false;
                        }
                    }
                    return isIntercept;
                }
            }
        }
        //不加的话 ReactScrollView滑动不了
        if (super.onInterceptTouchEvent(ev)) {
            NativeGestureUtil.notifyNativeGestureStarted(this, ev);
            ReactScrollViewHelper.emitScrollBeginDragEvent(this);
            mDragging = true;
            enableFpsListener();
            return true;
        }
        return false;
    }

以上代码完成了第一步创建原生固定滚动控件主要逻辑。

- scrollViewDidScroll:(UIScrollView *)scrollView{ CGFloat bottomCellOffset = [_tableView rectForSection:1].origin.y - 64; if (scrollView.contentOffset.y >= bottomCellOffset) { scrollView.contentOffset = CGPointMake(0, bottomCellOffset); if (self.canScroll) { self.canScroll = NO; self.contentCell.cellCanScroll = YES; } }else{ if (!self.canScroll) {//子视图没到顶部 scrollView.contentOffset = CGPointMake(0, bottomCellOffset); } } self.tableView.showsVerticalScrollIndicator = _canScroll?YES:NO;}

一、最外层tableview

2.创建管理滚动控件ViewManager的子类

简单讲下,copy RN自带的ScrollViewManager 类,修改类名和其他引用到ScrollViewManager 。另外注意修改字段,REACT_CLASS = "RNFixedScrollView",这个与JS的模块的名字存在映射。

上面代码中的canScroll是定义的一个BOOL变量用来控制mainTableView的偏移量

1.上下滑动的tableview

由于屏幕上有多个tableview,需要同时相应操作,所以这个tableview需要接受多个手势。需要新建一个tableview,继承自UITableView,遵守手势代理,运行接受多个手势。暂时称为大tableview,有个canScroll属性。
h文件

#import <UIKit/UIKit.h>

@interface MainTableView : UITableView <UIGestureRecognizerDelegate>

@end

m文件

#import "MainTableView.h"

@implementation MainTableView

//允许接受多个手势
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{

    return YES;
}

@end

还需要headView,sectionHeaderView等。

3.创建实现了ReactPackage接口的类并注册

RNAppViewsPackage 类

public class RNAppViewsPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(
            ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        return modules;
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Arrays.<ViewManager>asList(
                new RNFixedScrollViewManager()
        );
    }
}

MainApplication类进行注册

 @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
          new MainReactPackage(),
              new RNAppViewsPackage()
      );
    }

此时,subTableView收到可以滚动的信号cellCanScroll,才开始响应滑动的手势,在subTableViewUIScrollViewDelegate方法内实现:

2.横向滑动的collectionview

由于最外层的tableview只起到上下滑动的作用,所以一个cell就够了。
cell的高度 = 屏幕高度 - 头view高度 - sectionHeader高度。
在这个cell上添加一个collectionview,横向滑动,collectionview大小和cell大小一致。
紧接着在这个collectionview上添加几个tableview,这几个tableview大小和collectionview大小一致,即和最外层唯一的大cell的大小一致。暂时称为小tableview.也有个canScroll属性。
添加小tableview的时候,用了addChildViewController方法,等于先添加子控制器,用了子控制器的tableview。

    self.subVC1 = [[SubTableVC alloc] init];
    [self addChildViewController:self.subVC1];

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{

    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"UICollectionViewCellID" forIndexPath:indexPath];
    cell.backgroundColor = [UIColor yellowColor];

    SubTableVC *subVC = self.childViewControllers[indexPath.row];
    subVC.view.frame = CGRectMake(0, 0, SCREENW, SCREENH-64-49-40);
    subVC.tableView.frame = CGRectMake(0, 0, SCREENW, SCREENH-64-49-40);
    [cell.contentView addSubview:subVC.view];
    return cell;
}

这样就完成了整体的结构。

4.实现对应的JavaScript模块

简单讲下,copy RN自带ScrollViewJS的module,修改注释上 providesModule 的值RNFixedScrollView以及导出原生模块的名称,与第二步的值存在映射。

if (Platform.OS === 'android') {
  nativeOnlyProps = {
    nativeOnly: {
      sendMomentumEvents: true,
    }
  };
  AndroidScrollView = requireNativeComponent(
    'RNFixedScrollView',
    (ScrollView: React.ComponentType<any>),
    nativeOnlyProps
  );
}

完成上面的内容后,可以通过导入 import RNFixedScrollView from './modules/RNFixedScrollView',使用 RNFixedScrollView 控件

- scrollViewDidScroll:(UIScrollView *)scrollView{ if (!self.vcCanScroll) { scrollView.contentOffset = CGPointZero; } if (scrollView.contentOffset.y <= 0) { if (!self.fingerIsTouch) { return; } self.vcCanScroll = NO; scrollView.contentOffset = CGPointZero; [[NSNotificationCenter defaultCenter] postNotificationName:@"leaveTop" object:nil];//到顶通知父视图改变状态 } self.tableView.showsVerticalScrollIndicator = _vcCanScroll?YES:NO;}

三、监听tableview的偏移量

直接根据scollview的代理方法,- (void)scrollViewDidScroll:(UIScrollView *)scrollView;监听大tableview的滑动。

这里是重点。慢慢滑动页面,慢慢体会。

大tableview的滑动:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

    if (scrollView == self.tableView) {

        CGFloat scrollY = [self.tableView rectForSection:0].origin.y;

        if (scrollView.contentOffset.y >= scrollY) {

            if (self.canScroll == YES) {

                self.canScroll = NO;

                self.subVC1.canScroll = YES;
                self.subVC1.tableView.contentOffset = CGPointZero;
                self.subVC2.canScroll = YES;
                self.subVC2.tableView.contentOffset = CGPointZero;
                self.subVC3.canScroll = YES;
                self.subVC3.tableView.contentOffset = CGPointZero;
            }

            self.tableView.contentOffset = CGPointMake(0, scrollY);

        }else{
            //这句判断,是在小tableview往滑动的时候,需要固定大tableview的偏移量,原因参考第6点。
            if (self.canScroll == NO) {
                self.tableView.contentOffset = CGPointMake(0, scrollY);
            }
        }
    }
}

过程分析:
1.根据实际需求,首次加载页面的时候,大tableview需要滑动,小tableview不需要滑动,这个时候是大tableview带着小tableview跑。所以大tableview的canScroll=YES,小tableview的canScroll=NO。两者的属性值一直是相反的。实际情况也只这样 ,滑动的时候,只需要一个滑动就够了。

2.等滑动头view消失的时候,大tableview就不需要滑动,小tableview滑动。
(scrollY是头tableview中第一个section的Y坐标。也就是头view需要消失的时候的偏移量)。

3.第一次向下滑动,从大tableview下滑动开始,直到头view消失之前,这个时间段这个代理方法不需要啥操作,这段时间是大tableview带着小tableview滑动。

4.等第一次滑动偏移量=scrollY的时候,,这个时候大tableview就不需要滑动了,该小tableview滑动了。所以这时候需要if (scrollView.contentOffset.y >= scrollY)这个判断开始起作用。

5.继续往下滑,都是小tableview在滑动了。看起来貌似没大tableview的事情了。接着就网上滑动了,需要回去了。

6.重点:虽然canScroll属性=NO,但不代表大小tableview不会滑动了,它每时每刻都在响应着scrollview的代理方法。它还会滑动,只是我们不让它滑动,不需要滑动的时候,我们就固定它的偏移量。(这点很重要)

小tableview滑动的监听
block是为了通知大tableview可以滑动了。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

    if (self.canScroll == NO) {
        scrollView.contentOffset = CGPointZero;
    }

    if (scrollView.contentOffset.y < 0 ) {
        self.canScroll = NO;
        scrollView.contentOffset = CGPointZero;
        self.block();
    }
}

7.页面初始化canScroll=NO,所以为了不让它滑动,就设置偏移量=0,等到可以滑动的时候,canScroll=YES。

8.该往上滑回到顶部了。第6点说过,tableview一直响应滑动的方法,所以一直网上滑,当scrollView.contentOffset.y < 0,说明小tableview到顶部了,就不需要它滑动了,设置canScroll=NO,通知大tableview可以滑动了。

9.在block通知中设置大tableview canScroll=YES,回到了初始状态,完成了整个过程。
demo

测试

为了模拟这个界面,构建了下面的代码,其中 ViewPagerPage组件是Tab导航控件,详细代码请转到 github。

  • 主页面
 <View style={styles.container}>
                <RNFixedScrollView showsVerticalScrollIndicator={false}>
                    <View style={{
                        backgroundColor: '#87cefa',
                        height: 200,
                    }}>
                    </View>
                    <ViewPagerPage style={{height: windowHeight- 80}}/>
                </RNFixedScrollView>
            </View>
  • Tab导航控件,第二个tab内容区域嵌套了 FlatList,其他两个则显示文字。
import {StyleSheet, View, Text, Platform, Image, TouchableOpacity, Animated, Dimensions, FlatList} from 'react-native';
import React, {Component} from 'react';
import {PagerTabIndicator, IndicatorViewPager, PagerTitleIndicator, PagerDotIndicator} from 'rn-viewpager';

const windowWidth = Dimensions.get('window').width;
export default class ViewPagerPage extends Component {

    static title = '<FlatList>';
    static description = 'Performant, scrollable list of data.';

    state = {
        data: this.genItemData(20,0),
        debug: false,
        horizontal: false,
        filterText: '',
        fixedHeight: true,
        logViewable: false,
        virtualized: true,
    };

    genItemData(loadNum,counts){
       let items = [];
       for(let i=counts;i<counts loadNum;i  ){
           items.push({key:i});
        }
        return items;
    };

    _onEndReached(){
        this.setState((state) => ({
            data: state.data.concat(this.genItemData(10, state.data.length)),
        }));
    };

    render() {
        return (

                <IndicatorViewPager
                    style={[{backgroundColor: 'white', flexDirection: 'column-reverse'},this.props.style]}
                    indicator={this._renderTitleIndicator()}
                >
                    <View style={{backgroundColor: 'cornflowerblue'}}>
                        <Text>这里是课程介绍</Text>
                    </View>
                    <View style={{backgroundColor: 'cadetblue'}}>
                        <FlatList
                            ItemSeparatorComponent={() => <View
                                style={{height: 1, backgroundColor: 'black', marginLeft: 0}}/>}
                            data={this.state.data}
                            onEndReached={this._onEndReached.bind(this)}
                            onEndReachedThreshold={0.2}
                            renderItem={({item}) => <View
                                style={{  justifyContent: 'center',height:40,alignItems:'center'}}><Text
                                style={{fontSize: 16}}>{"目录" item.key}</Text></View>}
                        />
                    </View>
                    <View style={{backgroundColor: '#1AA094'}}>
                        <Text>相关课程</Text>
                    </View>
                </IndicatorViewPager>

        );
    }

    _renderTitleIndicator() {
        return <PagerTitleIndicator style={{
            backgroundColor: 0x00000020,
            height: 48
        }} trackScroll={true} itemStyle={{width: windowWidth / 3}}
                                    selectedItemStyle={{width: windowWidth / 3}} titles={['详情介绍', '目录', '相关课程']}/>;
    }


}

其实到这里会发现主要是通过设置两个scrollView的偏移量来解决这个手势冲突的问题。因为代码已经表达的很清楚,不太擅长表达,这里只是大概描述一下使用场景。

总结

  • 从编写玩这个组件在RN组件封装还是很有收获的,对于衡量使用不同的方案进行选择也有了体会。
  • 调试代码的时候需要技巧,通过注释不同的代码段,对于渲染不出界面是一种好的方法。
  • 弄清楚原理后编码会少犯很多错误。

具体的实现方式已经写成了一个demo,里面也有详细的注释,可以前去下载:FSScrollViewNestTableView

参考:

讲讲Android事件拦截机制
Android 屏幕手势滑动

2017.6.21--应网友要求,demo添加了刷新逻辑

给大家看一下实现后的效果图:

图片 10FSScrollViewNestTableView.gif图片 111792044-836ac7f747b647ed.jpg

本文由新葡京8455发布,转载请注明来源

关键词: