当前位置: 澳门新濠3559 > 服务器运维 > 正文

外部无法进行调用,可以展示比设备屏幕更大区

时间:2019-12-08 19:30来源:服务器运维
本文实例讲解原生JavaScript实现滚动条效果的相关代码,分享给大家供大家参考,具体内容如下 引导 UIScrollView释义:滚动视图( 核心功能显示在滚动和缩放;所有可滑动视图的基类 )

本文实例讲解原生JavaScript实现滚动条效果的相关代码,分享给大家供大家参考,具体内容如下

引导


UIScrollView 释义:滚动视图(核心功能显示在滚动和缩放;所有可滑动视图的基类)。UIScrollView 是一个非常重要的控件,UITableViewUICollectionViewUITextView等常用控件全部继承自UIScrollView,而UIWebView控件内部也是基于UIScrollView实现的。而UIScrollView继承自UIView,至于它可以滚动的原因我下面会有原理说明。

澳门新濠3559 1

本篇文章主要从 [UIScrollView .h文件 / 场景思维 / 总结笔记] 整理,该模块将系统化学习,后替换、补充文章内容 ~
在「时间 & 知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出槽点,以提高文章质量@白开水ln著;

目录:

  1. UIScrollView API
  2. UIScrollView .h文件
  3. UIScrollView 使用分析
  4. UIScrollView 场景思维
    1.导航栏半透明效果
    2.控件导航悬停
    3.下拉头部图片放大
    4.无限轮播(3张 || 2张)
  5. UIPageControl 小圆点
  6. UIScrollView 总结笔记
    1.处理触摸事件原理
    2.设计的代理属性为什么是weak?
    3.UIScrollView底层实现
  7. UIScrollView.h
  8. SourceCode 、 ToolsClass、WechatPublic-Codeidea

作者: ztelur
联系方式:segmentfault,csdn,github

原理是对滑动条块进行监听,按下鼠标按键后,监听鼠标移动,然后根据滑动条块移动的百分比算出滚动区域的滚动程度,用marginLeft进行滚动,具体的写在注释里。

UIScrollView API


澳门新濠3559 2

释义:可以展示比设备屏幕更大区域的内容,我们可以通过手指滑动来查看内容视图(content view)的每一部分内容,也可以通过手指捏合来对内容视图进行缩放操作。

注解:1、UIScrollView是UIKit中为数不多响应滑动手势的View(内置手势识别器Pan、Pinch);使用场景:显示不下(单张大图),内容太多(图文混排),广告轮播(图片),相册浏览,嵌套并用等;2、UIScrollView 没有循环利用;

本文仅供个人学习,不用于任何形式商业目的,转载请注明原作者、文章来源,链接,版权归原文作者所有。

整体弄成了一个对象,防止各种乱七八糟的数据污染全局变量。另外,对象内部调用的函数也都写到了对象构造函数的里面,由于对象作用域链的原理,外部无法进行调用,防止不小心在外部调用。

UIScrollView .h文件


warning - 以下为功能模块相关的方法示例, 具体方法作用、注解、使用请移步 -> github.com/CoderLN/Framework-Codeidea

 #warning - 以下为功能模块相关的方法示例, 具体方法作用、注解、使用请移步 -> github.com/CoderLN/Framework-Codeidea

 - - -

 #pragma mark - 枚举 & 常量

 typedef NS_ENUM(NSInteger, UIScrollViewIndicatorStyle) {
    UIScrollViewIndicatorStyleDefault, // 黑内容白边框,适用于任何背景
    UIScrollViewIndicatorStyleBlack,  // 全黑
    UIScrollViewIndicatorStyleWhite  // 全白
 }; // 设置滚动条的样式

 typedef NS_ENUM(NSInteger, UIScrollViewKeyboardDismissMode) {
    UIScrollViewKeyboardDismissModeNone, // 不隐藏键盘
    UIScrollViewKeyboardDismissModeOnDrag,  // 当拖拽scrollView时隐藏键盘
    UIScrollViewKeyboardDismissModeInteractive,// 当拖拽键盘上方时隐藏键盘
 } NS_ENUM_AVAILABLE_IOS(7_0);// 键盘的消失模式


 - - -

 #pragma mark - 常用属性
 #pragma mark - 内容视图属性/方法

/**
 作用: 内容视图大小, 是指定scollView能显示的内容的大小
 使用:
    self.scrollView.contentSize = CGSizeMake(image.size.width, image.size.height);
 注解:
    如果不设置contentSize的大小默认contentSize的大小就是scrollView的view的frame的大小
 */
@property(nonatomic)         CGSize                       contentSize;                    // default CGSizeZero


/**
 作用: 内容偏移量, (xy: 上左为正,下右为负)
 使用:
    [self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, 100) animated:YES];
 注解:
    contentOffset = scrollView.frame.size.width左上角 - contentSize.width内容视图的左上角
    x轴不滑动: 可以写成 self.scrollView.contentOffset.x 不建议直接写成0
    y轴滑动:   y = 100 向上滑动偏移100(显示下部内容)
 */
@property(nonatomic)         CGPoint                      contentOffset;                  // default CGPointZero


/**
 作用: 内边距, (上左下右: 向里为正,向外为负), 在内容周围额外增加的间距始终粘着内容;
 使用:
    self.scrollView.contentInset = UIEdgeInsetsMake(20, 20, 20, 20);
 */
@property(nonatomic)         UIEdgeInsets                 contentInset;


- - -

#pragma mark - 滑动属性方法

/**
 作用:是否允许多个方向(水平/竖直)同时滑动,默认为NO,
 注解:
    如果设置为YES,用户在水平/竖直方向开始进行滑动,便禁止同时在竖直/水平方向滑动
 */
@property(nonatomic,getter=isDirectionalLockEnabled) BOOL directionalLockEnabled;

/**
 作用:是否有触底反弹效果(可以看到背景颜色),默认为YES
 */
@property(nonatomic)         BOOL                         bounces;

- - -

#pragma mark - 指示器属性方法

/**
 作用:是否在滑动时指示器可见【水平】,默认为YES
 */
@property(nonatomic)         BOOL                         showsHorizontalScrollIndicator;

/**
 作用:是否在滑动时指示器可见【竖直】,默认为YES
 */
@property(nonatomic)         BOOL                         showsVerticalScrollIndicator;

/**
 作用:为指示器周围增加可滚动区域【滚动条内边距】,默认为UIEdgeInsetsZero
 */
@property(nonatomic)         UIEdgeInsets                 scrollIndicatorInsets;

/**
 作用:指示器样式,默认为UIScrollViewIndicatorStyleDefault(黑内容白边框,适用于任何背景), Black(全黑)、White(全白)
 */
@property(nonatomic)         UIScrollViewIndicatorStyle   indicatorStyle;

/**
 作用:闪一下指示器(注: 建议在scrollView展示给用户时调用一下,以提醒用户该处可滑动)
 */
- (void)flashScrollIndicators;

- - -

#pragma mark - 内置手势识别器
/**
 作用:拖动手势
 */
@property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer NS_AVAILABLE_IOS(5_0);
/**
 作用:捏合手势
 */
@property(nullable, nonatomic, readonly) UIPinchGestureRecognizer *pinchGestureRecognizer NS_AVAILABLE_IOS(5_0);
/**
 作用:定向手势识别器
 */
@property(nonatomic, readonly) UIGestureRecognizer *directionalPressGestureRecognizer, tvos(9.0, 11.0));

- - -

#pragma mark - 事件属性方法

- - -

#pragma mark - 缩放属性方法

- - -

#pragma mark - 键盘的消失模式

- - -

#pragma mark - 缩放代理方法

- - -

#pragma mark - 滑动代理方法
@optional (可选)

/**
 作用:只要scrollView滚动就会调用该方法
 注解:
    由于该方法在视图滚动中一直调用,所以不要在这里做耗时的计算
 */
- (void)scrollViewDidScroll:(UIScrollView *)scrollView; 

/**
 作用:手指滑动: 将要开始拖动调用, 停止定时器
 注解:
    该方法可能需要先滑动一段时间或距离才会被调用
 */
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;

/**
 作用:手指滑动: 减速完成调用, 开启定时器
 注解:
    只要设置了scrollView的分页显示,当手动(使用手指)滚动结束后,该代理方法会被调用
 */
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;

/**
 作用:当-setContentOffset:animated:/-scrollRectVisible:animated:方法动画结束时调用(仅当animated设置为YES时才调用)
 */
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView;

- - -

#pragma mark - iOS(11.0)

/**
 注解:ios11 之前,不想让scrollView偏移64px,设置automaticallyAdjustsScrollViewInsets=NO就可以了。
 ios11以后就废弃了,使用scrollView的属性contentInsetAdjustmentBehavior来防止偏移
 */
typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
    UIScrollViewContentInsetAdjustmentAutomatic,// 自动计算内边距. 
    UIScrollViewContentInsetAdjustmentScrollableAxes,// 自动计算内边距. 
    UIScrollViewContentInsetAdjustmentNever,// 不计算内边距  
    UIScrollViewContentInsetAdjustmentAlways,// 根据safeAreaInsets 计算内边距  
} API_AVAILABLE(ios(11.0),tvos(11.0));
*/

本文是android滚动相关的系列文章的第二篇,主要总结一下使用手势相关的代码逻辑。主要是单点拖动,多点拖动,fling和OveScroll的实现。每个手势都会有代码片段。
 对android滚动相关的知识还不太了解的同学可以先阅读一下文章:

 Blank Page for Rich Text Editing  .outer{width:500px;border:1px solid black;overflow:hidden;margin:50px 0 0 100px;} .test_div{width:1200px;background-image:linear-gradient(90deg,lightcoral 0%,lightgreen 50%,lightblue 100%);height:150px;} .slider_bar,.slider_block{ border-radius:5px;} .slider_bar{position:relative;width:80%;margin:5px auto 5px auto;background-color:lightgreen;height:5px;} .slider_block{width:20px;height:5px;background-color:grey;cursor:pointer;position:absolute;} window.onload=function(){ /** * 滑动条对象构造函数, * 内含其他功能性函数,利用函数作用域链的原理,防止自己随意调用 * 兼容:firefox、opera、chrome * ie没试,然而显然不兼容旧版本ie,因为旧版本ie添加事件监听函数的方法不同。如若要兼容ie,还需要添加其他函数 * js生成的滑动条类名为slider_bar、滑动块类型为slider_block,可用css样式自己设置大小、颜色等。 * 滑动条左右padding未限制滑动条界限,如若需要限制,须在计算部分进行细小修改,加算padding,此处略去。 * * @param {DOMElement} slider_content 被滚动的元素 */ function Slider{ //slider_instance为对象本身(在事件处理函数中会进行访问,而事件处理函数中的this对象已被注入为event.currentTarget,因此预先存储) var slider_instance=this; //this.slider_content为被滚动的元素 this.slider_content=slider_content; //this.outer为被滚动元素的父元素 this.outer=slider_content.parentNode; //创建滑动条 this.slider_bar=createSliderBar(); //创建滑动条块 this.slider_block=createSliderBlock(); //拼装 this.slider_bar.appendChild; this.outer.appendChild; //被滚动元素可被滚动的总宽度 this.slider_content_width=this.slider_content.offsetWidth-this.outer.clientWidth; //滑动条块可滑动的总宽度 this.slider_bar_width=this.slider_bar.clientWidth-this.slider_block.offsetWidth; //被滚动元素的左边距 this.slider_content_left=0; //滚动块的左边距 this.slider_block_left=0; //滑动条的左边距 this.slider_bar_pageLeft=getPageLeft; //滑动条块添加鼠标压键事件 this.slider_block.addEventListener("mousedown",mousedownHandler,false); //离开父元素后取消鼠标移动事件 this.outer.addEventListener("mouseleave",mouseupHandler,false); //鼠标弹键时取消鼠标移动事件 this.outer.addEventListener("mouseup",mouseupHandler,false); /** * 创建滑动条 */ function createSliderBar(){ var slider_bar=document.createElement; slider_bar.className="slider_bar"; return slider_bar; } /** * 创建滑动条块 */ function createSliderBlock(){ var slider_block=document.createElement; slider_block.className="slider_block"; return slider_block } /** * 鼠标按下事件处理 */ function mousedownHandler{ //计算鼠标相对滑动块的左边距,进而在鼠标移动事件处理函数中使用 //鼠标相对滑动块左边距=鼠标相对视口左边距-滑动块相对视口左边距 slider_instance.mouseLeft=event.clientX-getPageLeft; console.log; slider_instance.outer.addEventListener("mousemove",mousemoveHandler,false); } /** * 鼠标移动事件处理 */ function mousemoveHandler{ //计算出应当设置的滑动块左边距 //滑动块相对于滑动条左边距=鼠标相对于视口左边距-滑动条相对于视口左边距-鼠标相对于滑动块左边距 var blockLeft=event.clientX-slider_instance.slider_bar_pageLeft-slider_instance.mouseLeft; //如若滑动块相对于父容器左边距大于滑动块可移动宽度或小于0,表示过界;设置为左右界限值 if(blockLeft>slider_instance.slider_bar_width){ blockLeft=slider_instance.slider_bar_width }else if{ blockLeft=0; } //设置滑动块的新位置 slider_instance.slider_block.style.left=blockLeft+"px"; //按照滚动块已滚动的百分比,设置被滚动元素的marginLeft,进而让其滚动起来 //被滚动元素的左margin=-(滑动块相对于滑动条左边距/可滑动最大宽度*可滚动元素的最大宽度) slider_instance.slider_content.style.marginLeft="-"+(blockLeft/slider_instance.slider_bar_width*slider_instance.slider_content_width)+"px"; } /** * 鼠标键弹起事件处理 */ function mouseupHandler{ slider_instance.outer.removeEventListener("mousemove",mousemoveHandler,false); } /** * 获得元素的视口左边距 */ function getPageLeft{ var result=el.offsetLeft; var parent=el.offsetParent; while{ result+=parent.offsetLeft; parent=parent.offsetParent; } return result; } } //用test_div元素进行展示 new Slider(document.getElementsByClassName; } 

UIScrollView 使用分析


对于 UIScrollView 的使用,前提是要对重要的属性 contentSize、 contentOffset、 contentInset,要有正确的认知。

  • 文字注解:
/**
 对重要的属性 **contentSize、 contentOffset、 contentInset**,要有正确的认知。

 contentSize.height : 所有内容的总高度

 contentInset : 在内容周围额外增加的间距(内边距),始终粘着内容

 ontentOffset : 内容距离frame矩形框,偏移了多少

 contentOffset.x == frame最左边 的差值 - contentSize最左边; (左滑正 - 右滑负)

 contentOffset.y == frame顶部 -  contentSize顶部; (上滑正 - 下滑负)

 frame : 是以父控件内容的左上角为坐标原点{0, 0}

 bounds : 是以自己内容的左上角为坐标原点{0, 0}
 */
  • 图解:
![](https://upload-images.jianshu.io/upload_images/2230763-e65d004464d0f58f.png)
  • 《Android-MotionEvent详解》
  • 《Android Scroll详解(一):基础知识》

希望本文所述对大家学习javascript程序设计有所帮助。

UIScrollView 场景思维


为了节约你的时间,我特地将文章大致内容总结如下:

导航栏半透明效果

分析:

  • 默认情况下,在有UINavigationBar存在时,系统为了防止UIScrollView被遮挡,其contentInsetscrollIndicatorInsets属性都会被设置为UIEdgeInsetsMake(64, 0, 0, 0);在有UITabBar存在时,系统为了防止UIScrollView被遮挡,其contentInsetscrollIndicatorInsets属性都会被设置为UIEdgeInsetsMake(0, 0, 49, 0)

  • 因此,为了使用此种半透明效果,可以直接将UIScrollViewframe设置为整个屏幕的大小。

  • 注1: 系统只在UIScrollView是控制器视图的第0个子视图时才会自动修改contentInsetscrollIndicatorInsets属性。

  • 注2: 如果不想让系统自动修改contentInsetscrollIndicatorInsets属性,可以设置self.automaticallyAdjustsScrollViewInsets = NO;


  • 手势Drag的实现和原理
  • 手势Fling的实现和原理
  • OverScroll效果和EdgeEffect效果的实现和原理。
控件导航悬停

分析:

  • 通过-scrollViewDidScroll:代理方法跟踪contentOffset的的变化,当不满足悬停条件时,待悬停控件属于UIScrollView的子视图,当满足悬停条件时,待悬停控件属于UIScrollView的父视图的子视图。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
   if (scrollView.contentOffset.y >= 100)
   {
       CGRect rect = label.frame;
       rect.origin.y = 0;
       label.frame = rect;

       [self.view addSubview:label];
   }
   else
   {
       CGRect rect = label.frame;
       rect.origin.y = 100;
       label.frame = rect;

       [scrollView addSubview:label];
   }
}

详细代码请查看我的github

下拉头部图片放大

分析:

  • 通过-scrollViewDidScroll:代理方法跟踪contentOffset的的变化,根据contentOffset动态设置图片的缩放比例。
// 以"动态修改图片缩放比例于1倍和2倍之间"为例
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
   CGFloat scale = 1 - (scrollView.contentOffset.y / 100);
   scale = (scale >= 1) ? scale : 1;
   scale = (scale <= 2) ? scale : 2;
   imageView.transform = CGAffineTransformMakeScale(scale, scale);
}

Drag

Drag是最为基本的手势:用户可以使用手指在屏幕上滑动,以拖动屏幕相应内容移动。实现Drag手势其实很简单,步骤如下:

  • ACTION_DOWN事件发生时,调用getXgetY函数获得事件发生的x,y坐标值,并记录在mLastXmLastY变量中。
  • ACTION_MOVE事件发生时,调用getXgetY函数获得事件发生的x,y坐标值,将其与mLastXmLastY比较,如果二者差值大于一定限制(ScaledTouchSlop),就执行scrollBy函数,进行滚动,最后更新mLastXmLastY的值。
  • ACTION_UPACTION_CANCEL事件发生时,清空mLastXmLastY
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int actionId = MotionEventCompat.getActionMasked(event);
        switch (actionId) {
            case MotionEvent.ACTION_DOWN:
                mLastX = event.getX();
                mLastY = event.getY();
                mIsBeingDragged = true;
                if (getParent() != null) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                float curX = event.getX();
                float curY = event.getY();
                int deltaX = (int) (mLastX - curX);
                int deltaY = (int) (mLastY - curY);
                if (!mIsBeingDragged && (Math.abs(deltaX)> mTouchSlop ||
                                                        Math.abs(deltaY)> mTouchSlop)) {
                    mIsBeingDragged = true;
                    // 让第一次滑动的距离和之后的距离不至于差距太大
                    // 因为第一次必须>TouchSlop,之后则是直接滑动
                    if (deltaX > 0) {
                        deltaX -= mTouchSlop;
                    } else {
                        deltaX += mTouchSlop;
                    }
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                // 当mIsBeingDragged为true时,就不用判断> touchSlopg啦,不然会导致滚动是一段一段的
                // 不是很连续
                if (mIsBeingDragged) {
                        scrollBy(deltaX, deltaY);
                        mLastX = curX;
                        mLastY = curY;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mIsBeingDragged = false;
                mLastY = 0;
                mLastX = 0;
                break;
            default:
        }
        return mIsBeingDragged;
    }
无限轮播(3张)

分析:

澳门新濠3559 3

  • 手动滑动:只创建三张图片: leftImageView、 centerImageView 、 rightImageView ,定义中间显示页码为 centerPage ,设置初始self.centerPage = 0;,而左右图片页码都以 centerPage 表示,通过 scrollViewDidEndDecelerating: 方法判断contentOffsest.xscrollView.frame.size.width 比较,使改变 self.centerPage的值,然后对应页码滚动取数组中的图片,始终显示中间一页centerPage,只是切换显示的图片
  • 定时器滚动:将定时器添加到 RunloopNSRunLoopCommonModes下,触发方法中设置setContentOffset: animated:YES 方法动画结束时调用 scrollViewDidEndScrollingAnimation:(仅当animated设置为YES时才调用),这个方法下 self.centerPage++;。OK了

  • 监听图片点击
    当图片被点击的时候,我们往往需要执行某些操作,因此需要监听图片的点击,思路如下:
    1、定义一个block属性暴露给外界void(^LNBlock)(NSInteger index)
    2、设置centerImageViewuserInteractionEnabledYES
    3、给scrollView添加一个点击的手势
    4、在手势方法里调用block,并传入图片索引

  • 核心代码

#pragma mark - 添加三张图片
- (void)createContentViews
{
    // 把3张imageView对象添加到scrollView上
    CGRect frame = self.bounds;
    self.leftImageView = [[UIImageView alloc] initWithFrame:frame];

    frame.origin.x += ScreenViewW;
    self.centerImageView = [[UIImageView alloc] initWithFrame:frame];

    frame.origin.x += ScreenViewW;
    self.rightImageView = [[UIImageView alloc] initWithFrame:frame];

    [self.scrollView addSubview:self.leftImageView];
    [self.scrollView addSubview:self.centerImageView];
    [self.scrollView addSubview:self.rightImageView];
}


#pragma mark - set方法赋值
- (void)setCenterPage:(NSInteger)centerPage {
    _centerPage = centerPage;
    if (_centerPage < 0) { // 向右滑动显示左面图片,值与0比较
        _centerPage = self.imageArray.count - 1;
    }
    if (_centerPage > self.imageArray.count - 1) {
        _centerPage = 0; // 向左滑动显示右面图片,值与count-1 比较
    }

    // Page: left和right用center表示
    NSInteger leftPage = _centerPage - 1 < 0 ? self.imageArray.count -1 : _centerPage - 1;
    NSInteger rightPage = _centerPage +1 > self.imageArray.count - 1 ? 0 : _centerPage + 1;

    // 赋值
    self.leftImageView.image = self.imageArray[leftPage];
    self.centerImageView.image = self.imageArray[_centerPage];
    self.rightImageView.image = self.imageArray[rightPage];

    // 显示中间那一页(注:这里不要使用动画)
    [self.scrollView setContentOffset:CGPointMake(self.scrollView.frame.size.width, 0)];
    // 设置pageControl的页码
    self.pageControl.currentPage = _centerPage;

}

#pragma mark - UIScrollViewDelegate

// 手动滚动 减速完毕会调用(停止滚动),开启定时器
// 只要设置了scrollView的分页显示,当手动(使用手指)滚动结束后,该代理方法会被调用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    // 判断contentOffsest.x
    if (scrollView.contentOffset.x > scrollView.frame.size.width) { // 下一张
        self.centerPage++;
    } else if (scrollView.contentOffset.x < scrollView.frame.size.width){ // 上一张
        self.centerPage--;
    }
    [self startTimer];
    NSLog(@"手动减速完毕,开启定时器 当前页 %ld",self.centerPage);
}
  • 效果图
![](https://upload-images.jianshu.io/upload_images/2230763-00289fbfbd03c415.gif)

多触点Drag

上边的代码只适用于单点触控的手势,如果你是两个手指触摸屏幕,那么它只会根据你第一个手指滑动的情况来进行屏幕滚动。更为致命的是,当你先松开第一个手指时,由于我们少监听了ACTION_POINTER_UP事件,将会导致屏幕突然滚动一大段距离,因为第二个手指移动事件的x,y值会和第一个手指移动时留下的mLastXmLastY比较,导致屏幕滚动。

如果我们要监听并处理多触点的事件,我们还需要对ACTION_POINTER_DOWNACTION_POINTER_UP事件进行监听,并且在ACTION_MOVE事件时,要记录所有触摸点事件发生的x,y值。

  • ACTION_POINTER_DOWN事件发生时,我们要记录第二触摸点事件发生的x,y值为mSecondaryLastXmSecondaryLastY,和第二触摸点pointer的id为mSecondaryPointerId
  • ACTION_MOVE事件发生时,我们除了根据第一触摸点pointer的x,y值进行滚动外,也要更新mSecondayLastXmSecondaryLastY
  • ACTION_POINTER_UP事件发生时,我们要先判断是哪个触摸点手指被抬起来啦,如果是第一触摸点,那么我们就将坐标值和pointer的id都更换为第二触摸点的数据;如果是第二触摸点,就只要重置一下数据即可。
        switch (actionId) {
            .....
            case MotionEvent.ACTION_POINTER_DOWN:
                activePointerIndex = MotionEventCompat.getActionIndex(event);
                mSecondaryPointerId = MotionEventCompat.findPointerIndex(event,activePointerIndex);
                mSecondaryLastX = MotionEventCompat.getX(event,activePointerIndex);
                mSecondaryLastY = MotionEventCompat.getY(event,mActivePointerId);
                break;
            case MotionEvent.ACTION_MOVE:
                ......
                // handle secondary pointer move
                if (mSecondaryPointerId != INVALID_ID) {
                    int mSecondaryPointerIndex = MotionEventCompat.findPointerIndex(event, mSecondaryPointerId);
                    mSecondaryLastX = MotionEventCompat.getX(event, mSecondaryPointerIndex);
                    mSecondaryLastY = MotionEventCompat.getY(event, mSecondaryPointerIndex);
                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                //判断是否是activePointer up了
                activePointerIndex = MotionEventCompat.getActionIndex(event);
                int curPointerId  = MotionEventCompat.getPointerId(event,activePointerIndex);
                Log.e(TAG, "onTouchEvent: "+activePointerIndex +" "+curPointerId +" activeId"+mActivePointerId+
                                        "secondaryId"+mSecondaryPointerId);
                if (curPointerId == mActivePointerId) { // active pointer up
                    mActivePointerId = mSecondaryPointerId;
                    mLastX = mSecondaryLastX;
                    mLastY = mSecondaryLastY;
                    mSecondaryPointerId = INVALID_ID;
                    mSecondaryLastY = 0;
                    mSecondaryLastX = 0;
                    //重复代码,为了让逻辑看起来更加清晰
                } else{ //如果是secondary pointer up
                    mSecondaryPointerId = INVALID_ID;
                    mSecondaryLastY = 0;
                    mSecondaryLastX = 0;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mIsBeingDragged = false;
                mActivePointerId = INVALID_ID;
                mLastY = 0;
                mLastX = 0;
                break;
            default:
        }
无限轮播(2张)

分析:

  • 最底层是一个UIView,上面有一个UIScrollView以及UIPageControlscrollView上有两个UIImageViewimageView宽高 = scrollview宽高 = view宽高;

  • 假设轮播控件的宽度为x高度为y,我们设置scrollviewcontentSize.width3x,并让scrollview的水平偏移量为x,既显示最中间内容。

  • 具体细节请移步这里阅读:轮播两个ImageView实现 http://www.jianshu.com/p/ef03ec7f23b2

Fling

当用户手指快速划过屏幕,然后快速立刻屏幕时,系统会判定用户执行了一个Fling手势。视图会快速滚动,并且在手指立刻屏幕之后也会滚动一段时间。Drag表示手指滑动多少距离,界面跟着显示多少距离,而fling是根据你的滑动方向与轻重,还会自动滑动一段距离。Filing手势在android交互设计中应用非常广泛:电子书的滑动翻页、ListView滑动删除item、滑动解锁等。所以如何检测用户的fling手势是非常重要的。
 在检测Fling时,你需要检测手指在屏幕上滑动的速度,这是你就需要VelocityTrackerScroller这两个类啦。

  • 我们首先使用VelocityTracker.obtain()这个方法获得其实例
  • 然后每次处理触摸时间时,我们将触摸事件通过addMovement方法传递给它
  • 最后在处理ACTION_UP事件时,我们通过computeCurrentVelocity方法获得滑动速度;
  • 我们判断滑动速度是否大于一定数值(MinFlingSpeed),如果大于,那么我们调用Scrollerfling方法。然后调用invalidate()函数。
  • 我们需要重载computeScroll方法,在这个方法内,我们调用ScrollercomputeScrollOffset()方法啦计算当前的偏移量,然后获得偏移量,并调用scrollTo函数,最后调用postInvalidate()函数。
  • 除了上述的操作外,我们需要在处理ACTION_DOWN事件时,对屏幕当前状态进行判断,如果屏幕现在正在滚动(用户刚进行了Fling手势),我们需要停止屏幕滚动。

具体这一套流程是如何运转的,我会在下一篇文章中详细解释,大家也可以自己查阅代码或者google来搞懂其中的原理。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        .....
        if (mVelocityTracker == null) {
            //检查速度测量器,如果为null,获得一个
            mVelocityTracker = VelocityTracker.obtain();
        }
        int action = MotionEventCompat.getActionMasked(event);
        int index = -1;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                ......
                                if (!mScroller.isFinished()) { //fling
                    mScroller.abortAnimation();
                }
                .....
                break;
            case MotionEvent.ACTION_MOVE:
                ......
                break;
            case MotionEvent.ACTION_CANCEL:
                endDrag();
                break;
            case MotionEvent.ACTION_UP:
                if (mIsBeingDragged) {
                //当手指立刻屏幕时,获得速度,作为fling的初始速度     mVelocityTracker.computeCurrentVelocity(1000,mMaxFlingSpeed);
                    int initialVelocity = (int)mVelocityTracker.getYVelocity(mActivePointerId);
                    if (Math.abs(initialVelocity) > mMinFlingSpeed) {
                        // 由于坐标轴正方向问题,要加负号。
                        doFling(-initialVelocity);
                    }
                    endDrag();
                }
                break;
            default:
        }
        //每次onTouchEvent处理Event时,都将event交给时间
        //测量器
        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(event);
        }
        return true;
    }
    private void doFling(int speed) {
        if (mScroller == null) {
            return;
        }
        mScroller.fling(0,getScrollY(),0,speed,0,0,-500,10000);
        invalidate();
    }
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            postInvalidate();
        }
    }
UIPageControl 小圆点

  • 基础使用
// pageControl创建
- (UIPageControl *)pageControl
{
    if(_pageControl == nil) {
        _pageControl = [[LNPageControl alloc] init];
        _pageControl.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.2];
        _pageControl.numberOfPages = self.images.count; // 多少页码
        _pageControl.currentPage = self.centerPage; // 当前页码
        _pageControl.hidesForSinglePage = YES; // 单页时隐藏
        _pageControl.pageWH = 10;// 设置圆点大小

        // 自定义小圆点(图片),KVC 访问私有变量
        _pageControl.pageIndicatorTintColor = [UIColor lightGrayColor]; // 页码颜色
        _pageControl.currentPageIndicatorTintColor = [UIColor whiteColor]; // 当前页码颜色
        [_pageControl setValue:[UIImage imageNamed:@"dot_1"] forKeyPath:@"_currentPageImage"];
        [_pageControl setValue:[UIImage imageNamed:@"dot"] forKeyPath:@"_pageImage"];
    }
    return _pageControl;
}



// 手指滑动: 减速完成调用, 开启定时器 (只要设置了 paggingEnabled 分页显示,手指滑动后就会调用)
// 判断scrollView.contentOffset.x 与 kviewWidth,使用 self.centerPage++; 或 self.centerPage--;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    self.pageControl.currentPage = self.centerPage;
    NSLog(@"手指减速完成,开启定时器 当前 %ld 页",self.centerPage);
}

// 代码滚动: setContentOffset: animated:YES 时才会调用
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
    self.centerPage++;
    self.pageControl.currentPage = self.centerPage;
}
  • 自定义使用
/**
 @interface LNPageControl : UIPageControl
 自定义PageControl小圆点大小
 */
// 方法一: 重新设置子控件frame
- (void)layoutSubviews {
    [super layoutSubviews];

    // 外部设置frame 和 pageW ,效果是圆点平分frame显示
    CGFloat spacingW = (self.frame.size.width - self.subviews.count * self.pageWH) /(self.subviews.count +1);

    for (NSInteger index = 0 ; index<self.subviews.count; index++) {

        CGFloat pageX = (index + 1) * spacingW + index * self.pageWH + self.pageWH/2;
        UIImageView *subView = [self.subviews objectAtIndex:index];
        if (index == self.currentPage)  {
            subView.bounds = CGRectMake(0, 0, self.pageWH+5, self.pageWH+5);
            subView.center = CGPointMake(pageX, self.frame.size.height /2);
            subView.layer.cornerRadius = (self.pageWH+5) / 2;
            subView.layer.masksToBounds = YES;
        } else {
            subView.bounds = CGRectMake(0, 0, self.pageWH, self.pageWH);
            subView.center = CGPointMake(pageX, self.frame.size.height /2);
            subView.layer.cornerRadius = self.pageWH / 2;
            subView.layer.masksToBounds = YES;
        }
    }
}

- - -
- - -

// 方法二: 重写CurrentPage,设置圆点的大小
- (void)setCurrentPage:(NSInteger)currentPage
{
    [super setCurrentPage:currentPage];

    // 外部设置frame 和 pageW ,效果是圆点平分frame显示
    CGFloat spacingW = (self.frame.size.width - self.subviews.count * self.pageWH) /(self.subviews.count +1);
    for (NSInteger index = 0; index < self.subviews.count; index++) {

        UIImageView * pageView = [self.subviews objectAtIndex:index];
        CGFloat pageViewX = (index + 1) * spacingW + index * self.pageWH + self.pageWH/2;
        if (index == self.currentPage)  {
            pageView.bounds = CGRectMake(0, 0, self.pageWH+5, self.pageWH+5);
            pageView.center = CGPointMake(pageViewX, self.frame.size.height /2);
        } else {
            pageView.bounds = CGRectMake(0, 0, self.pageWH, self.pageWH);
            pageView.center = CGPointMake(pageViewX, self.frame.size.height /2);
        }
    }
}

OverScroll

在Android手机上,当我们滚动屏幕内容到达内容边界时,如果再滚动就会有一个发光效果。而且界面会进行滚动一小段距离之后再回复原位,这些效果是如何实现的呢?我们需要使用ScrollerscrollTo的升级版OverScrolleroverScrollBy了,还有发光的EdgeEffect类。
 我们先来了解一下相关的API,理解了这些接口参数的含义,你就可以轻松使用这些接口来实现上述的效果啦。

protected boolean overScrollBy(int deltaX, int deltaY,
            int scrollX, int scrollY,
            int scrollRangeX, int scrollRangeY,
            int maxOverScrollX, int maxOverScrollY,
            boolean isTouchEvent)
  • int deltaX,int deltaY : 偏移量,也就是当前要滚动的x,y值。
  • int scrollX,int scrollY : 当前的mScrollX和mScrollY的值。
  • int maxOverScrollX,int maxOverScrollY: 标示可以滚动的最大的x,y值,也就是你视图真实的长和宽。也就是说,你的视图可视大小可能是100,100,但是视图中的内容的大小为200,200,所以,上述两个值就为200,200
  • int maxOverScrollX,int maxOverScrollY:允许超过滚动范围的最大值,x方向的滚动范围就是0maxOverScrollX,y方向的滚动范围就是0maxOverScrollY。
  • boolean isTouchEvent:是否在onTouchEvent中调用的这个函数。所以,当你在computeScroll中调用这个函数时,就可以传入false。
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)
  • int scrollX,int scrollY:就是x,y方向的滚动距离,就相当于mScrollXmScrollY。你既可以直接把二者赋值给相应的成员变量,也可以使用scrollTo函数。
  • boolean clampedX,boolean clampY:表示是否到达超出滚动范围的最大值。如果为true,就需要调用OverScrollspringBack函数来让视图回复原来位置。
public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)
  • int startX,int startY:标示当前的滚动值,也就是mScrollXmScrollY的值。
  • int minX,int maxX:标示x方向的合理滚动值
  • int minY,int maxY:标示y方向的合理滚动值。

相信看完上述的API之后,大家会有很多的疑惑,所以这里我来举个例子。
 假设视图大小为100*100。当你一直下拉到视图上边缘,然后在下拉,这时,mScrollY已经达到或者超过正常的滚动范围的最小值了,也就是0,但是你的maxOverScrollY传入的是10,所以,mScrollY最小可以到达-10,最大可以为110。所以,你可以继续下拉。等到mScrollY到达或者超过-10时,clampedY就为true,标示视图已经达到可以OverScroll的边界,需要回滚到正常滚动范围,所以你调用springBack(0,0,0,100)。

然后我们再来看一下发光效果是如何实现的。
 使用EdgeEffect类。一般来说,当你只上下滚动时,你只需要两个EdgeEffect实例,分别代表上边界和下边界的发光效果。你需要在下面两个情景下改变EdgeEffect的状态,然后在draw()方法中绘制EdgeEffect

  • 处理ACTION_MOVE时,如果发现y方向的滚动值超过了正常范围的最小值时,你需要调用上边界实例的onPull方法。如果是超过最大值,那么就是调用下边界的onPull方法。
  • computeScroll函数中,也就是说Fling手势执行过程中,如果发现y方向的滚动值超过正常范围时的最小值时,调用onAbsorb函数。

然后就是重载draw方法,让EdgeEffect实例在画布上绘制自己。你会发现,你必须对画布进行移动或者旋转来让EdgeEffect澳门新濠3559,绘制出上边界或者下边界的发光的效果,因为EdgeEffect对象自己是没有上下左右的概念的。

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        if (mEdgeEffectTop != null) {
            final int scrollY = getScrollY();
            if (!mEdgeEffectTop.isFinished()) {
                final int count = canvas.save();
                final int width = getWidth() - getPaddingLeft() - getPaddingRight();
                canvas.translate(getPaddingLeft(),Math.min(0,scrollY));
                mEdgeEffectTop.setSize(width,getHeight());
                if (mEdgeEffectTop.draw(canvas)) {
                    postInvalidate();
                }
                canvas.restoreToCount(count);
            }

        }
        if (mEdgeEffectBottom != null) {
            final int scrollY = getScrollY();
            if (!mEdgeEffectBottom.isFinished()) {
                final int count = canvas.save();
                final int width = getWidth() - getPaddingLeft() - getPaddingRight();
                canvas.translate(-width+getPaddingLeft(),Math.max(getScrollRange(),scrollY)+getHeight());
                canvas.rotate(180,width,0);
                mEdgeEffectBottom.setSize(width,getHeight());
                if (mEdgeEffectBottom.draw(canvas)) {
                    postInvalidate();
                }
                canvas.restoreToCount(count);
            }

        }
    }

 @Override
    public boolean onTouchEvent(MotionEvent event) {
            ......
            case MotionEvent.ACTION_MOVE:
                .....
                if (mIsBeingDragged) {
                    overScrollBy(0,(int)deltaY,0,getScrollY(),0,getScrollRange(),0,mOverScrollDistance,true);
                    final int pulledToY = (int)(getScrollY()+deltaY);
                    mLastY = y;
                    if (pulledToY<0) {
                        mEdgeEffectTop.onPull(deltaY/getHeight(),event.getX(mActivePointerId)/getWidth());
                        if (!mEdgeEffectBottom.isFinished()) {
                            mEdgeEffectBottom.onRelease();
                        }
                    } else if(pulledToY> getScrollRange()) {
                        mEdgeEffectBottom.onPull(deltaY/getHeight(),1.0f-event.getX(mActivePointerId)/getWidth());
                        if (!mEdgeEffectTop.isFinished()) {
                            mEdgeEffectTop.onRelease();
                        }
                    }
                    if (mEdgeEffectTop != null && mEdgeEffectBottom != null &&(!mEdgeEffectTop.isFinished()
                                        || !mEdgeEffectBottom.isFinished())) {
                        postInvalidate();
                    }
                }
                .....
        }
        ....
    }
    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        if (!mScroller.isFinished()) {  
            int oldX = getScrollX();
            int oldY = getScrollY();
            scrollTo(scrollX,scrollY);
            onScrollChanged(scrollX,scrollY,oldX,oldY);
            if (clampedY) {
                Log.e("TEST1","springBack");
                mScroller.springBack(getScrollX(),getScrollY(),0,0,0,getScrollRange());
            }
        } else {
            // TouchEvent中的overScroll调用
            super.scrollTo(scrollX,scrollY);
        }
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int oldX = getScrollX();
            int oldY = getScrollY();
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();

            int range = getScrollRange();
            if (oldX != x || oldY != y) {
                overScrollBy(x-oldX,y-oldY,oldX,oldY,0,range,0,mOverFlingDistance,false);
            }
            final int overScrollMode = getOverScrollMode();
            final boolean canOverScroll = overScrollMode == OVER_SCROLL_ALWAYS ||
                    (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
            if (canOverScroll) {
                if (y<0 && oldY >= 0) {
                    mEdgeEffectTop.onAbsorb((int)mScroller.getCurrVelocity());
                } else if (y> range && oldY < range) {
                    mEdgeEffectBottom.onAbsorb((int)mScroller.getCurrVelocity());
                }
            }
        }
    }

UIScrollView 总结笔记


后记

本篇文章是系列文章的第二篇,大家可能已经知道如何实现各类手势,但是对其中的机制和原理还不是很了解,之后的第三篇会讲解从本篇代码的视角讲解一下android视图绘制的原理和Scroller的机制,希望大家多多关注。

UIScrollView处理触摸事件原理

当用户在UIScrollView的一个子视图上按下时,UIScrollView并不知道用户是想要滑动内容视图还是点击对应子视图,所以在按下的一瞬间,事件UIEventUIApplication传递到UIScrollView后,其会先将该事件拦截而不会立即传递给对应的子视图,同时开始一个150ms的倒计时,并监听用户接下来的行为

  • 当倒计时结束前,如果用户的手指发生了移动,则直接滚动内容视图,不会将该事件传递给对应的子视图;

  • 当倒计时结束时,如果用户的手指位置没有改变,则调用自身的-touchesShouldBegin:withEvent:inContentView:方法询问是否将事件传递给对应的子视图(如果返回NO,则该事件不会传递给对应的子视图,如果返回YES,则该事件会传递给对应的子视图,默认为YES)。

  • 当事件被传递给子视图后,如果手指位置又发生了移动,则调用自身的-touchesShouldCancelInContentView:方法询问是否取消已经传递给子视图的事件。

参考文章

http://stackoverflow.com/questions/22843671/android-swipe-vs-fling

https://www.google.com/design/spec/patterns/gestures.html#gestures-drag-swipe-or-fling-details

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1212/2145.html

设计的代理属性为什么是weak?

澳门新濠3559 4

图解:

  • 原因:防止造成循环引用❓
  • 注解:
    1、rootViewController 指针指向控制器对象(每个控制器都有个View 被 强引用) --> UIView对象( 强引用 内部 属性 subViews) --> 子控件数组对象( 强引用 假设scrollView对象在第0位) --> UIScrollView对象将ViewController设置为自己的代理(delegate),所以的用 弱引用 weak。
    2、假设没有指针指向这个控制器,而delegate(weak弱引用) 没有强指针引用,控制器就会释放,相反如果设置代理使用强引用则会造成循环引用。

UIScrollView底层实现

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIScrollView : UIView <NSCoding>


#pragma mark ------------------
#pragma mark - 内置手势识别器

/** 拖动手势 */
@property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer NS_AVAILABLE_IOS(5_0);

/** 捏合手势 */
@property(nullable, nonatomic, readonly) UIPinchGestureRecognizer *pinchGestureRecognizer NS_AVAILABLE_IOS(5_0);

/** 定向手势识别器 */
@property(nonatomic, readonly) UIGestureRecognizer *directionalPressGestureRecognizer UIKIT_AVAILABLE_TVOS_ONLY(9_0);
  • 注解:
    UIScrollView 继承自 UIView,内部加了手势,之所以可以滑动,是改变了bounds,若手指向上滑动,y++,内容就会向上滑动。这样我们也可以用 给UIView 添加个 Pan手势 实现滚动效果。

参考:http://www.jianshu.com/p/ba88e12eddc2

附上阶段总结写的小样 Demo,重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现 !

Reading


  • 如果在阅读过程中遇到 Error || New ideas,希望你能 issue 我,我会及时补充谢谢。
  • 熬夜写者不易,喜欢可 赞赏 or Star 一波;点击左上角关注 或 『微众 Codeidea』,在 Demo || 文章 更新时收到提醒通知,便捷你的阅读。

编辑:服务器运维 本文来源:外部无法进行调用,可以展示比设备屏幕更大区

关键词: