Viewer源码阅读心得4——用户交互

版权所有, 转载请著明出处,保留链接。
Viewer源码阅读心得4——用户交互

##用户交互概述
用户交互, 即用户在APP上操作, APP呈现出不同的形态。在Cocoa Touch中, UIView继承自UIResponder, UIViewController也继承于UIResponder,而我们在APP页面上看到的都是UIView, 无论是UIViewController,还是UIView,都能够接受处理交互, 而在页面视图层次结构中, 肯定是点击所在的视图先接受到事件, 再把事件向其控制器传递, 向父视图传递, 直到UIApplication。那么是在哪里处理捕获这个事件, 并对事件进行处理呢?这就需要根据视图的组合, 和数据的所在层次, 做相应的权衡。大体上可分为两种,一种是从上到下, 即父控制器(父视图)接受到事件, 它处理完事件的相关部分,涉及到自身相关的, 再把事件传递给相关的子视图, 即直接调用它的处理函数, 逐层的处理下去;

UIApplication
|------->UIViewController
        |------->UIView
                    |-------->UIViewController
                              |--------->UIView

还有一种是从下到上, 即由子视图或控制器捕获事件, 由它处理完成, 再把该事件传递给父视图或者父控制器,其传递的方式, 父控制器或者父视图往往设置为子视图或者子控制器的委托, 而子视图或者控制器捕获事件时, 直接调用委托协议的方法, 将该交互事件传递上去。

UIApplication
|<-------UIViewController
        |<-------UIView
                    |<--------UIViewController
                              |<---------UIView

Viewer中刚好两种方式都有体现, 在LibraryViewController中的LibraryDirectoryCell, LibraryDocumentsCell,则是从下到上的交互传递;而ReaderViewController中的手势识别的交互处理则展现了从上到下的交互方式。那么在什么情况下, 采用那种方式呢? 这我也没分析出来, 跟视图,数据的层级组合有关, 下面请看Viewer中LibraryViewController,ReaderViewController的详细分析.

##LibraryViewController

LibraryViewController控制器, 如下是其视图层次结构,下图是在其UIView的UIScrollView的子视图:

 LibraryDirectoryView  LibraryDocumentsView

|------------------- | --------------------|
|  UIXToolbarView          UIXToolbarView
|--------------------| --------------------|
|  ReaderThumbsView          ReaderThumbsView  
|                    |                     |
|                    |                     |
|                    |                     |
|                    |                     |
|                    |                     |
|                    |                     |
|--------------------| --------------------| 

LibraryViewController作为视图事件的顶层处理对象, 当事件传递到这边时, 通过委托协议逐级向上层传播,并在事件的冒泡传递过程中, 如果能在这一视图层级处理掉的, 涉及数据和视图的呈现, 则在这一层级处理掉, 如果还需要向上层传递, 则通过委托继续传递上去。在LibraryViewController中就有2个这样的交互事件处理:

- (void)directoryView:(LibraryDirectoryView *)directoryView didSelectDocumentFolder:(DocumentFolder *)folder
- (void)documentsView:(LibraryDocumentsView *)documentsView didSelectReaderDocument:(ReaderDocument *)document
  1. 一个是LibraryDirectoryCell tap的时候传递上来的,这时候,LibraryViewController要显示LibraryDocumentsView, 有它来显示folder下的文档信息;
  2. 一个是LibraryDocumentsCell tap的时候传递上啦的,这时候,LibraryViewController要把tap的文档信息传递给新实例化出的readerViewController,有它来显示ReaderDocument。

##LibraryDirectoryView中的UIXToolbarView
在LibraryDirectoryView目录视图的UIXToolbarView工具栏视图上接受的用户事件有:

  1. 用户点击工具栏上的帮组信息按钮;
  2. 用户点击工具栏上的创建文件夹按钮;
  3. 用户点击工具栏上的进入编辑模式按钮;
  4. 用户在编辑模式下点击重名文件夹按钮;
  5. 用户在编辑模式下点击删除文件夹按钮;
  6. 用户在编辑模式下点击退出编辑按钮;

当用户点击这些按钮时, UIXToolbarView视图首先接受到事件,那么在哪里处理这些事件呢?可以看出这些用户点击事件, 影响的视图表现是UIXToolbarView和ReaderThumbsView视图,而LibraryDirectoryView视图是它们的父视图, 因此这些事件的处理点都是在LibraryDirectoryView中处理:

infoButton            ----->  infoButtonTapped      显示帮组信息
theCheckButton        ----->  checkButtonTapped     编辑模式处理
theEditButton        ----->    editButtonTapped      重命名文件夹
thePlusButton        ----->  plusButtonTapped      创建文件夹 

##LibraryDirectoryView中的ReaderThumbsView

    ReaderThumbsView
|----------------------    |
| LibraryDirectoryCell  |
| LibraryDirectoryCell  | 
|     .....                |
|                        |
|                        |
|                        |
|----------------------    |

ReaderThumbsView继承于UIScrollView,在UIScrollView上放置多个LibraryDirectoryCell,自身添加了手势识别处理事件,tap事件和长按事件:

tap      -----> handleTapGesture
press    -----> handlePressGesture

handleTapGesture的处理逻辑如下:

  1. 首先找到触摸的手势的点point;
  2. 根据点找到触摸的是哪个LibraryDirectoryCell;
  3. 再告诉ReaderThumbsView的委托有个文件夹被点击了,叫它做出相应的反应;[实际上LibraryDirectoryView做出反应]
  4. 如果不在编辑模式,则把这个tap事件传递给LibraryDirectoryView的委托[实际上是LibraryViewController]
    如果在编辑模式,则更新tap单元的状态;

handlePressGesture的处理逻辑如下:

  1. 首先找到长按的手势的点point;
  2. 根据点找到长按的是哪个LibraryDirectoryCell;
  3. 再告诉ReaderThumbsView的委托有个文件夹被长按了,叫它做出相应的反应;[实际上是LibraryDirectoryView做出反应]
  4. 翻转编辑模式,如果是在编辑模式下,把长按的文件夹对象加入到选择的文件夹集合中,并更新状态;

由于LibraryDirectoryCell是作为ReaderThumbsView的子视图, 而ReaderThumbsView继承于UIScrollView,而当视图上下滑动时, 会显示不同位置的LibraryDirectoryCell, 处理滑动了的事件逻辑如下:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView.
  1. 如果有滑动, 对比scrollView的contentOffset有无变动;
  2. 如果有滑动, 则对thumbCellsVisible数组中的LibraryDirectoryCell进行处理, 如果其是在当前bounds范围内,则加入可见数组,如果没在,则入队;
  3. 获取偏移可见位置区的LibraryDirectoryCell,对每个索引进行判断, 如果是在thumbCellsVisible中,则不要获取,如果不是,则计算其矩形区域,并出对一个ReaderThumbView,更新其状态;

那么同理, LibraryDocumentsView中UIXToolbarView和ReaderThumbsView的用户交互实现也是同样的道理。

##ReaderViewController
ReaderViewController也是一个视图控制器, 它的视图层次结构如下,下图也是在其UIView的子视图:

          UIView
|-------------------------|
|        mainToolbar          |
|                          |    
|        theScrollView      |
|                          |
|                          |
|        mainPagebar          |            
|-------------------------|

                        theScrollView
|--------------------------|-----------------------|
|                           |                         |
|                          |                       |
|    ReaderContentView       |   ReaderContentView   |
|                           |                       |
|                           |                       |
|                           |                       |
|--------------------------|-----------------------|

它上有手势识别的动作,分别是singleTapOne, doubleTapOne, doubleTapTwo, 他们的对应的处理函数如下:

singleTapOne  ----> handleSingleTap:
doubleTapOne  ----> handleDoubleTap:
doubleTapTwo  ----> handleDoubleTap:

handleSingleTap的逻辑处理如下:

  1. 当手势被识别的时候开始处理逻辑;
  2. 如果手势的tap的点在处理的区域内, 则进行如下处理;
  3. 从contentViews数组中获取当前页面的ReaderContentView, 把手势参数传递给它, 叫它处理;
  4. ReaderContentView处理返回的对象target如果不为nil, 则判断其是否是NSURL对象, 如果是,构建NSURL的模式,调用[[UIApplication sharedApplication] openURL:url] 打开该链接。该函数可能会调用其他APP哦!如果不是,则判断是否是NSNumber对象,如果是, 则调用showDocumentPage显示该页面;
  5. 如果ReaderContentView处理返回的对象target如果为nil,则根据距离最后一次工具栏,阅读进度栏的隐藏事件,来隐藏或显示工具栏;
  6. 如果触碰的点在下一页的区域内, 则调用incrementPageNumber 翻到下一页;
  7. 如果触碰到的点在上一页的区域内, 则调用decrementPageNumber 翻到上一页;

handleDoubleTap的处理逻辑如下:

  1. 当手势被识别的时候才还是处理逻辑:recognizer.state == UIGestureRecognizerStateRecognized
  2. 如果手势的tap的点在处理的区域内, 则进行如下处理;
  3. 从contentViews数组中获取当前页面的ReaderContentView,如果单个手指的, 则调用ReaderContentView的放大操作;
    如果是两个手指的, 则调用缩小操作
    ;
  4. 如果触碰的点在下一页的区域内, 则调用incrementPageNumber 翻到下一页;
  5. 如果触碰到的点在上一页的区域内, 则调用decrementPageNumber 翻到上一页;

而ReaderViewController中的ReaderMainToolbar,当用户点击上面的按钮时, 发生交互, 这些交互的实际处理点也在ReaderViewController中, 只是ReaderMainToolbar通过委托协议,当用户点击按钮时, ReaderMainToolbar会调用delegate的相应处理方法, 而这里ReaderViewController就是其delegate。

Done按钮        doneButton        - (void)tappedInToolbar:(ReaderMainToolbar *)toolbar doneButton:(UIButton *)button
thumbs按钮    thumbsButton    - (void)tappedInToolbar:(ReaderMainToolbar *)toolbar thumbsButton:(UIButton *)button
mark按钮        markButton        - (void)tappedInToolbar:(ReaderMainToolbar *)toolbar markButton:(UIButton *)button

具体处理逻辑在这里就不详细描述了, 那么为什么需要在ReaderViewController中处理呢, 把点击的事件实际处理者交给了它的管理者?

##ReaderViewController中的ReaderContentView

而ReaderContentView继承于UIScrollView

            UIScrollView
    |---------------------------|
    |                            |
ReaderContentView中的theContainerView,
    |-------------------------- |
    |                            |
    |                            |
    |theThumbView/theContentPage|
    |                            |
    |                            |
    |                            |
    |                            |
    |-------------------------- |
    |                            |
    |---------------------------|

ReaderContentView继承于UIScrollView, 因此它本身默认具有与用户交互的能力,除此外, 它接受来自ReaderViewController的调用, 当它与用户交互时候(单击, 放大, 缩小), 处理单击, 其是把单击的手势处理操作传给子类theContentPage处理, 即调用[theContentPage processSingleTap:recognizer], 在ReaderContentPage中,判断点击到了链接, 如果点击到了, 则返回链接对象;而处理放大缩小是在ReaderContentView自身中处理,即放大或者缩小tap点矩形区域到指定倍数到ReaderContentView的UIView中。

##ReaderViewController中的ReaderMainPagebar

         ReaderMainPagebar的UIView
|-----------------------------------------    |
|                pageNumberView                |
|                                            |
|                trackControl                |
|-----------------------------------------    |


               ReaderTrackControl
|--------------    |-------------|------------    |
|                |                |                |
|ReaderPagebarThumb       ReaderPagebarThumb    |
|                |pageThumbView|                |
|--------------    |-------------|------------    |

ReaderMainPagebar的子视图ReaderTrackConrol,继承于UIControl,它重写了如下方法:

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event      
            //记录开始跟踪的value值,即点击点的X坐标
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event   
            //记录跟踪value值,并发送UIControlEventValueChanged事件
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event        
            //记录结束跟踪的value值,即点击点的X坐标

并且它接受如下事件,并且有相应的处理方法:

UIControlEventTouchDown            trackViewTouchDown:
UIControlEventValueChanged        trackViewValueChanged:
UIControlEventTouchUpOutside    trackViewTouchUp:
UIControlEventTouchUpInside        trackViewTouchUp:

在trackViewTouchDown:中, 逻辑如下:

  1. 首先计算touch的点所对应的页面;
  2. 然后根据页数,更新pageNumberView的显示, 更新pageThumbView的显示;
  3. 重新设置跟踪定时器,0.25秒, 当定时器触发时,调用trackTimerFired:方法;
  4. trackTimerFired:的作用是,在0.25秒过后,向ReaderMainPagebar的delegate发出[delegate pagebar:self gotoPage:trackControl.tag],跳转到指定页面。

在trackViewTouchUp:中, 逻辑如下:

  1. 使trackTimer的定时器无效;
  2. 如果触摸抬起的点不是文档的当前页, 则向ReaderMainPagebar的委托deletgate发出[delegate pagebar:self gotoPage:trackView.tag],并设置ReaderTrackControl不能与用户交互:trackView.userInteractionEnabled = NO;
  3. 开启使能定时器enableTimer;如果使能定时器0.25秒后触发, 则调用enableTimerFired:,在fired的消息中,使enableTimer失效,并开启ReaderTrackControl与用户交互:trackControl.userInteractionEnabled = YES;

在trackViewValueChanged:消息中,逻辑与trackViewTouchDown:一样,只是它重新设定了开始定时器trackTimer;

版权所有, 转载请著明出处,保留链接。