背景:
根据策划案要求,实现目前辞书不同字典的纸质电子书查看功能,根据要求 服务端是会把每本字典以PDF的形式返回给客户端,并附带当前PDF上每个文字所在区域的坐标信息,根据策划案的要求客户端需要做一个类似电子书阅读器的功能,并且要满足以下要求:
每个字的有效区域如下面红框所示:
点击有效区域要弹出如图所示的弹窗 显示相应的文字:
点击弹窗跳转到对应的PDF处查阅:
从详情跳转到纸质书,让选中的字先阴影遮罩0.5秒后红色虚线框框住:
需求分析:
主要的要求基本就是上面介绍的这些,然后我们来分析下其中有哪些难点:
1.坐标的转换
分析:
服务端返回图片是一张高清的原始图片,上面每个文字的坐标信息也是相对于这个原始图大小来标注的,
对于我们客户端如果想要在iphone上显示图片,并且准确获取图片上的文字坐标信息,就需要我们将原始图片坐标转换成对应于当前屏幕的坐标。
2.手势识别用户点击区域的内容
分析:这个看起来似乎很简单,因为我们都知道可以给图片添加点击的手势,并且可以在点击后获取当前点击的屏幕坐标,的确是这样,但是如果是没有缩放的图的话,这个需求只要我们问题1完成就可以顺利解决,但是策划案要求的是可以 缩放 拖动 操作,并且用户在 缩放,拖动后点击文字依然可以准确提取对应的坐标的文字信息,这就是难点所在。
3.弹窗的显示
大家先看下UI效果图:
根据UI要求是要弹出一个蒙版,然后将弹窗显示在蒙版上方,并且要在点击区域的正上方,这个也是需要我们来准确获取弹窗的显示坐标,并且要考虑放大缩小后图片上的点 怎样和蒙版上的点对应起来,只有这样才可以准确显示。
4.PDF的浏览查看功能。
这个目前来看类似相册照片的查看,有好多类似的框架可以复用。但是我们要考虑的是如何将我们的需求和这些框架兼容起来,或者就是自己实现这个功能。
5.选中的字先阴影遮罩0.5秒后红色虚线框框住
这个问题的本质就是准确获取当前文字的区域 和 画虚线红框
问题解决:
问题一:坐标的转换
这个问题仔细分析就是一个比例的转换的过程,只要找到对应的转换公式就可以,直接上代码:
/**
获取相对当前屏幕的某两个点之间 覆盖区域的frame
@param originStartPoint 原始图片的起始点
@param originEndPoint 原始图片的结束点
@param imageName 原始图片名
@return 覆盖区域的frame
*/
- (CGRect)transfromRectByOriginStartPoint:(CGPoint)originStartPoint
originEndPoint:(CGPoint)originEndPoint
imageName:(NSString *)imageName {
UIImage *image = [UIImage imageNamed:imageName];
CGPoint realStartPoint= [self realPoint:originStartPoint image:image];
CGPoint realEndPoint = [self realPoint:originEndPoint image:image];
CGRect react = CGRectMake(realStartPoint.x, realStartPoint.y, realEndPoint.x-realStartPoint.x, realEndPoint.y-realStartPoint.y);
return react;
}
/**
获取相对当前屏幕真实坐标
@param originPoint 原始坐标
@param image 图片
@return 相对当前屏幕中的图片的真实坐标
KBottomToolHeight 底部工具条的高
KNavHeight 导航栏的高
SCREEN_WIDTH 屏幕的宽
SCREEN_HEIGHT 屏幕的高
*/
- (CGPoint)realPoint:(CGPoint)originPoint image:(UIImage *)image {
CGFloat x = originPoint.x * SCREEN_WIDTH / image.size.width;
CGFloat y = originPoint.y * (SCREEN_HEIGHT - KBottomToolHeight) / image.size.height;
return CGPointMake(x, y);
}
这里强调一点,这个获取是 在当前屏幕上没有缩放情况下的坐标。
问题二:
这个问题的难点就在怎么保证可以在用户放大 缩小 拖动后仍然可以准确获取点击区域的坐标。
首先值得欣慰的是 系统又给我们提供点击后获取点击点的坐标:
- (CGPoint)locationInView:(nullable UIView*)view; // a generic single-point location for the gesture. usually the centroid of the touches involved
通过这个方法我们可以在手势的响应方法中,通过 UITapGestureRecognizer 类 来调用这个方法 ,获取点击点相对当前屏幕的坐标,然后可以用这个坐标和问题一中 我们转换的坐标进行对比,如果在有效区域内,就可以找到对应的文字信息。这些在没有缩放 拖动的情况下都是可以的,但是如果用户缩放 ,拖动图片的话,情况就比较复杂了。这里有两种方案可以选择:
方案一:
我们在 用户缩放 拖动 图片后 对当前图片上的文字信息坐标进行一次转换,每一次用户点击的时候都要根据当前图片的状态,把原始坐标转换成适合当前图片状态的坐标,然后点击的时候根据点击的坐标,在去跟转换后的坐标逐一对比找到符合的点,提取对应的文字信息。
分析:方案一的优缺点
优点:我们获取的是种相对当前屏幕的坐标,容易对后续的弹窗显示,和第三方图片浏览控件的对接。
缺点:计算量大,用户每操作一次缩放 拖动的动作 都要进行图片的坐标转换,这样会影响性能和界面流畅性,还有最致命的一点是:每次图片坐标信息相对当前屏幕的坐标转换的公式,数学规律,较难总结。耗时耗力。
方案二:
细心的人可能会发现locationInView这个方法是可以让我们传递相对视图的,一般我们都是传当前的view,获取相对屏幕的坐标,那我们如果相对当前的图片,那么缩放 拖动后,该图片的文字坐标信息 是不是不会变化呢?如果是的话我们就不用每次都去转换坐标,并且转换公式也比较简单。
分析:方案二的优缺点
经过验证,如果我们传的view 是当前的UIImageView 对象,那么无论在什么情况下,获取的图片上坐标点都是一致的,但是这里有个前提是:我们不能在缩放 拖动的过程中改变图片的size,否则坐标就不对了。
优点:不需要每次转换坐标,计算量小,性能比较好。
缺点:PDF的查看功能需要我们自己去实现兼容自己的需求,后面弹窗的显示坐标需要我们去转换成对应的屏幕坐标,特别是缩放,拖动后的显示坐标转换的计算方式总结 比较有难度。
问题三:弹窗的显示
根据问题二中的分析针对以上两种方案,基于应用的性能考虑我们一定是选择方案二,相对图片去获取坐标信息。但是这样就有一个十分棘手的问题需要我们去克服,根据UI效果图弹窗是显示在keywindow上的,并且放大后弹窗的大小是不变的,所以我们必须准确计算出放大后,当前屏幕内显示的图片内容相对屏幕的准确的位置信息。
为解决这个问题,首先我们要弄清楚放大后的图片,显示在屏幕内区域中的文字坐标信息,如何转换成相对屏幕的坐标信息。首先我们要明确一点:放大后的图片,中心点坐标是不变的(这里的放大系统是以中心点 为中心放大的),这样我们就可以根据移动前后的中心点坐标,获取到本次用户移动的距离,然后是不是可以得到图片上某一点的坐标,放大后该点在屏幕上的坐标,移动距离,这三者之间的关系呢?经过多次的尝试和总结终于 得到了二者之间的关系转换公式:
用户点击点坐标:P1(x,y),
缩放倍数:scale ,
移动前的中心点 center (a,b ) ,
移动后的中心点center1(c,d)
当前图片:currentImageView
CGFloat distance = CGPointMake(_oldCenter.x*scale-currentImageView.center.x ,_oldCenter.y*scale-
currentImageView.center.y);
x = self.wordMaskView.frame.origin.x * scale - distance.x; //当前屏幕显示的真实坐标x
y = self.wordMaskView.frame.origin.y * scale - distance.y; //当前屏幕显示的真实坐标y
经过上面的公式计算出来的用户点击坐标P1 就是相对于当前屏幕坐标系下的准确坐标,这样我们就可以在用户放大 缩小 移动 后准确获取到用户点击位置的坐标来显示弹窗。然后结合方案二中的结果去准确获取到当前位置的文字信息,就可以完美的显示用户点击处的内容,并用弹窗提示,在通过给弹窗的View添加点击手势事件 去完成跳转到对应的PDF内容页这个功能。
问题四:PDF的浏览查看功能
根据上面我们的讨论,PDF的浏览查看功能 可能就需要根据我们自己的需求去实现,目前的一些第三库实现图片浏览功能的库 会在用户缩放 移动图片的过程中 改变ImageView的Frame (包括Size),这样就会导致 我们获取坐标的值不准确,所以 项目中使用的是通过 给图片添加 拖动 缩放 的手势在结合ScrollerView来实现浏览查看的功能,这里面有一个难点就是如何控制图片放大后 用户移动的过程中 不让图片 移除屏幕,也就是永远让图片的所有可视区域只在屏幕内移动查看,网上也有类似的demo 但是那些都是在图片没有放大的时候控制图片只能在屏幕内移动而不会被拖出屏幕外。
经过研究发现 放大后的图片的新中心点 与 放大倍数 和一倍缩放情况下图片的中心点有一定的关系,下面是代码 后对应的代码:
// 处理拖拉手势
- (void)panView:(UIPanGestureRecognizer *)panGestureRecognizer {
CGFloat scale = self.paperImageView.frame.size.width / _oldFrame.size.width;
if (scale == 1 && self.paperImageView.center.x == _oldCenter.x && self.paperImageView.center.x == _oldCenter.x) {
self.paperImageView.center = _oldCenter;
return;
}
UIView*view=panGestureRecognizer.view;
if(panGestureRecognizer.state==UIGestureRecognizerStateBegan||panGestureRecognizer.state==UIGestureRecognizerStateChanged){
CGPoint translation=[panGestureRecognizer translationInView:view.superview];
CGPoint newcenter = CGPointMake(view.center.x+translation.x, view.center.y+translation.y);
if (newcenter.x >= 0) {
if (newcenter.x > scale*_oldCenter.x) {
newcenter.x = scale*_oldCenter.x;
if ([self.delegate respondsToSelector:@selector(isSide:)]) {
[self.delegate isSide:YES];
}
}
}else {
if (newcenter.x <= - (scale-1)*_oldCenter.x + _oldCenter.x) {
newcenter.x = - (scale-1)*_oldCenter.x + _oldCenter.x;
if ([self.delegate respondsToSelector:@selector(isSide:)]) {
[self.delegate isSide:YES];
}
}
}
if (newcenter.y >= 0) {
if (newcenter.y > scale*_oldCenter.y) {
newcenter.y = scale*_oldCenter.y;
}
}else {
if (newcenter.y <= - (scale-1)*_oldCenter.y + _oldCenter.y) {
newcenter.y = - (scale-1)*_oldCenter.y + _oldCenter.y;
}
}
[view setCenter:newcenter];
[panGestureRecognizer setTranslation:CGPointZero inView:view.superview];
}else if (panGestureRecognizer.state==UIGestureRecognizerStateEnded) {
_distance = CGPointMake(_oldCenter.x*scale-self.paperImageView.center.x ,_oldCenter.y*scale- self.paperImageView.center.y);
}
}
公式:
1.移动后的中心点在 为正数:
newcenter.y = scale*_oldCenter.y;
newcenter.x = scale*_oldCenter.x;
2.移动后的中心点为负数:
newcenter.y = - (scale-1)*_oldCenter.y + _oldCenter.y;
newcenter.x = - (scale-1)*_oldCenter.x + _oldCenter.x;
问题五:选中的字先阴影遮罩0.5秒后红色虚线框框住
这个问题 获取坐标 上面的分析中已经可以获取到,这里主要想分享一下 画红框的一些相关问题:
画红框可以分为两种方式去画:
1.在知道文字所在区域的正方形的Frame的情况下我们可以 计算出 矩形框四个顶点的坐标,然后一画一画的去绘制虚线框,这种比较好理解,就是比较麻烦。
2.第二种就是在已知Frame的情况下利用系统的CAShapeLayer 和 UIBezierPath 去绘制矩形虚线框,看下代码:
/**
绘制红色虚线
**/
- (void)drawDashLine:(CGRect)lineFrame index:(NSInteger)index{
//这里要注意 绘制虚线矩形框一定要依附于一个view,不然显示会有问题
UIView *lineView = [[UIView alloc]init];
lineView.frame = lineFrame;
lineView.tag = KRedLineViewTag + index;
lineView.backgroundColor = [UIColor clearColor];
lineView.userInteractionEnabled = NO;
[self.paperImageView addSubview:lineView];
CAShapeLayer *border = [CAShapeLayer layer];
border.strokeColor = [[UIColor colorWithHexString:@"#d91b23"] CGColor];
border.fillColor = nil;
border.path = [UIBezierPath bezierPathWithRect:lineView.bounds].CGPath;
border.frame = lineView.bounds;
border.lineWidth = 1.f;
border.lineCap = @"square";
border.lineDashPattern = @[@4, @3];
[lineView.layer addSublayer:border];
}
上面代码中我们要注意以下几点:
- 绘制虚线矩形框一定要依附于一个view,不然显示会有问题
2.view一定要添加在当前的imageview上,不然缩放 拖动的情况下虚线框的大小,位置不会随动。
3.因绘制虚线框需要依附于一个view,所以会让padf的手势无法响应,所以我们需要设置 lineView.userInteractionEnabled = NO;让点击事件不会被阻止。
总结:
本文主要从 以上四点 介绍了项目中实现电子书阅读器 手势识别选择区域内容的功能的主要难点,以及解决办法。这里谈点心得:
1.在遇到比较难一眼看出规律的现象时,我们要找一些特殊的值去试着找到一个可以符合要求的公式,然后在去看一般的数据,简单说就是从特殊到一般
2.要多去尝试,拓宽思维,在问题陷入思维僵持的时候 要去试着放弃一些当前思路,换个方向去尝试下。
3.就是要多讨论,与人讨论说出你的想法可以让你的思维活跃起来,推进问题的进展。
最后给大家推荐个不错的公众号 "说神码",或者大家可以扫描下面的二维码关注