DEMO的github地址:https://github.com/YYProgrammer/YYPhotoBrowserLikeWX
效果如下圖
- 實(shí)現(xiàn)圖片組的瀏覽奠骄,包含捏合縮放、雙擊縮放、單擊退出、向下拖拽退出等寒砖。
- 重點(diǎn)是“向下拖拽退出”的實(shí)現(xiàn)阅畴。
架構(gòu)設(shè)計(jì)
下文稱下圖中左邊的界面為界面A定页,右邊為界面B
界面A只是一個(gè)用來測試的界面踢械,界面B才是圖片瀏覽器拙泽,架構(gòu)設(shè)計(jì)主要針對界面B。主要需要考慮以下幾個(gè)點(diǎn):
- 界面B的結(jié)構(gòu):N張圖片需要左右滑動(dòng)裸燎、圖片本身需要縮放顾瞻、N種手勢交互、后期額外控件的添加等德绿。
- A到B荷荤、B到A的轉(zhuǎn)場動(dòng)畫。
- 重點(diǎn):向下拖拽的交互怎么實(shí)現(xiàn)
界面B的結(jié)構(gòu)
- N張圖片需要左右滑動(dòng):必然需要UIScrollView或其子類(UICollectionView)移稳,來放所有圖片蕴纳。
- 圖片本身需要縮放:所以圖片本身需要一個(gè)UIScrollView包裝起來用于做縮放。
- N種手勢交互:UIScrollView本身帶有很多手勢个粱,再往上添加手勢不妥古毛,所以應(yīng)當(dāng)創(chuàng)建個(gè)UIView,UIView里有UIScrollView都许,手勢加在UIView上稻薇,例如單擊、雙擊等胶征。
-
后期額外控件的添加:
額外控件.jpeg
例如上圖紅圈的控件塞椎,明顯不能添加到UIScrollView中否則就跟著滑走了。
最終設(shè)計(jì)如下圖:
- 藍(lán)色是個(gè)scrollview睛低,里面放圖片案狠,這樣可以縮放圖片。(demo中我把它封裝成了YYPhotoBrowserSubScrollView)
- 綠色是個(gè)scrollview钱雷,里面放藍(lán)色scrollview骂铁,這樣可以實(shí)現(xiàn)左右滑動(dòng)翻頁。(demo中我把它封裝成了YYPhotoBrowserMainScrollView)
- 紅色是個(gè)控制器的view罩抗,這樣做可以隨意添加額外控件拉庵,而且控制器的話,也方便做界面A-界面B的轉(zhuǎn)場動(dòng)畫(demo中對應(yīng)YYPhotoBrowserViewController)澄暮。
- 消息傳遞我采用的代理模式名段。
轉(zhuǎn)場動(dòng)畫
這里B是modal出的控制器,我把轉(zhuǎn)場效果交給了轉(zhuǎn)場代理transitioningDelegate泣懊,轉(zhuǎn)場動(dòng)畫具體做法可以參考文章:http://www.reibang.com/p/a65d3463f4bc
值得一提的是伸辟,拖拽時(shí)背景顏色透明,出現(xiàn)A的界面馍刮。換句話說信夫,B被present之后,A并沒有消失。這需要設(shè)置一個(gè)屬性
B.modalPresentationStyle = UIModalPresentationOverCurrentContext;
重點(diǎn):向下拖拽的交互的實(shí)現(xiàn)
控件簡介
結(jié)合上圖静稻,拖拽事件發(fā)生在藍(lán)色這一層警没。
藍(lán)色是個(gè)UIView(YYPhotoBrowserSubScrollView),添加了子控件UIScrollview(demo中對象命名為mainScrollView)振湾,mainScrollView的寬高占滿了YYPhotoBrowserSubScrollView杀迹。mainScrollView中有UIImageView。
雙擊手勢添加給YYPhotoBrowserSubScrollView押搪,雙擊后改變mainScrollView的zoomScale(縮放比例系數(shù))來實(shí)現(xiàn)縮放树酪,單擊手勢也添加給YYPhotoBrowserSubScrollView,單擊后通知代理-綠色控件大州,綠色再通知紅色控制器续语,控制器退回。
需求介紹
用戶向上拖拽時(shí)厦画,圖片向上移動(dòng)(即正常的scrollview的滾動(dòng)效果疮茄,結(jié)合demo中第一張長圖片查看)。
向下拖拽到最頂部并持續(xù)向下拖拽時(shí)根暑,圖片遂手勢移動(dòng)力试,并變小,背景逐漸透明购裙。松手瞬間懂版,如果手勢是向下移動(dòng)的,B頁退出躏率,如果手勢是向上移動(dòng)的,圖片回到原來的位置
解決方案分析
那么問題來了民鼓,拖拽的交互理所當(dāng)然動(dòng)用手勢-UIPanGestureRecognizer薇芝,添加給誰呢?
首先我們來看一下一個(gè)手勢發(fā)生時(shí)丰嘉,發(fā)生了哪些事情:
1夯到、生成一個(gè)UIEvent事件;
2饮亏、通過事件響應(yīng)鏈查找最合適的事件執(zhí)行者耍贾;
3、調(diào)用事件執(zhí)行者綁定的手勢事件路幸。
所以荐开,手勢所綁定的事件在執(zhí)行前,會(huì)先通過逐級查找的方式简肴,找到最適合響應(yīng)手勢的控件晃听,再執(zhí)行其方法,流程如下圖(白色原點(diǎn)處發(fā)生點(diǎn)擊)
圖中控件與上面的結(jié)構(gòu)圖的控件一致,藍(lán)色線是向下詢問過程能扒,橙色是返回結(jié)果的過程佣渴。
詢問過程:
1、application:window你好初斑,我收到一個(gè)事件(UIEvent)辛润,是在點(diǎn)你身上的,你看一看具體是你哪個(gè)孩子(subview)來響應(yīng)见秤,問好了告訴我砂竖。
2、window:我確認(rèn)了一下秦叛,我是可見的(hidden != NO && alpha >= 0.01)晦溪,我是可點(diǎn)擊的(userInteractionEnabled != NO),而且點(diǎn)擊的點(diǎn)確實(shí)在我身上([self pointInside:point withEvent:event] == YES)挣跋,那么三圆,我來遍歷一下我的孩子們(subview),看看誰最合適避咆,如果沒有的話舟肉,那就是我了。
3查库、4路媚、5、同上樊销。
返回過程:
1整慎、UIImageView:我不能被點(diǎn)擊。
2围苫、UIScrollview:我的孩子都不能響應(yīng)裤园,那就是我了。
3剂府、YYPhotoBrowserMainScrollView:UIScrollview可以拧揽。
4、5腺占、同上淤袜。
最后藍(lán)色那個(gè)UIScrollview就成了事件的響應(yīng)者。
其實(shí)這個(gè)詢問和返回的過程衰伯,就是UIView里的方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
按系統(tǒng)流程重寫的話铡羡,內(nèi)部過程如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 1.判斷當(dāng)前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2. 判斷點(diǎn)在不在當(dāng)前控件
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.從后往前遍歷自己的子控件(后添加的視圖在上面,在上面優(yōu)先響應(yīng))
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--)
{
UIView *childView = self.subviews[i];
//把當(dāng)前控件上的坐標(biāo)系轉(zhuǎn)換成子控件上的坐標(biāo)系
CGPoint childP = [self convertPoint:point toView:childView];
//讓subview繼續(xù)找它的subview
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView)//尋找到最合適的view
{
return fitView;
}
}
// 循環(huán)結(jié)束,表示沒有比自己更合適的view
return self;
}
然后嚎研,再調(diào)用響應(yīng)者綁定的對應(yīng)方法去執(zhí)行蓖墅,并且在調(diào)用時(shí)會(huì)將本次觸摸相關(guān)的等信息裝進(jìn)UIGestureRecognizer里作為參數(shù)库倘。
現(xiàn)在,我們來看一下這幾個(gè)不可行的方案:
- 添加一個(gè)pan手勢給UIImageView论矾,在手勢事件中判斷手勢方向教翩,如果使向上拖拽,把事件傳遞給scrollview贪壳,如果是向下饱亿,做“向下拖拽退出”交互效果。
- 不可行原因:
要判斷手勢方向闰靴,只能在手勢事件中進(jìn)行(hitTest中無法通過攜帶的UIEvent參數(shù)判斷方向)彪笼,通過方向來改變響應(yīng)者,而要改變手勢響應(yīng)者蚂且,只能在hitTest方法中配猫,但hitTest是在手勢事件執(zhí)行前,所以已經(jīng)確定了UIImageView為響應(yīng)者杏死,再想改變響應(yīng)者泵肄,UIImageView就會(huì)中斷對事件的響應(yīng),本次觸摸事件就宣告結(jié)束淑翼,需要再次抬起手指腐巢,重新下拉,生成新的事件玄括,這個(gè)時(shí)候才能讓scrollview來響應(yīng)冯丙。
- 不可行原因:
- 重寫scroolview的pan手勢的綁定事件。如果是向上拖動(dòng)遭京,就按系統(tǒng)的交互寫胃惜,如果是向下,就做“向下拖拽退出”交互效果哪雕。
無論是新建一個(gè)pan手勢覆蓋掉scroolview自帶的蛹疯,還是找到它自帶的pan手勢打印出它綁定的方法(沒記錯(cuò)的話叫handlePan:)然后重寫,還是自己用UIView的子類實(shí)現(xiàn)一個(gè)自定義Scrollview热监,都算作重寫,因?yàn)槎家约簩?shí)現(xiàn)系統(tǒng)的交互效果饮寞。- 不可行原因:
理論上不是不可行孝扛,是非常難。因?yàn)橄到y(tǒng)交互上幽崩,不只是單純的手指移動(dòng)苦始,內(nèi)容跟著動(dòng),還有:例如慌申,快速滑動(dòng)頁面后松手陌选,頁面會(huì)持續(xù)滑動(dòng)并以一個(gè)加速度做系數(shù)來減速直到停止理郑,這個(gè)加速度怎么算?又如:內(nèi)容滾到頂部后持續(xù)下拉再松手咨油,會(huì)像彈簧一樣彈回去您炉,回去的速度是變化的,這里的加速度又是多少役电?
- 不可行原因:
可行方案
思考題做到這里赚爵,其實(shí)答案已經(jīng)很接近了。
所有拖動(dòng)的交互都離不開pan手勢UIPanGestureRecognizer法瑟,所以UIScrollview既然能在手指移動(dòng)時(shí)做事兒冀膝,那它也離不開UIPanGestureRecognizer。
翻看UIScrollview的.h文件不難發(fā)現(xiàn)霎挟,它其實(shí)已經(jīng)暴露了它的pan手勢(不暴露就runtime遍歷屬性窝剖,總能找到想要的)。
上文說到酥夭,調(diào)用手勢綁定事件時(shí)赐纱,會(huì)把觸摸相關(guān)的信息放進(jìn)手勢對象里,所以手指在scrollview里移動(dòng)時(shí)采郎,就能從它的panGestureRecognizer拿到我們想要的信息:手指移動(dòng)路徑千所,就能根據(jù)這些數(shù)據(jù),做想要的效果蒜埋。
所以方案如下:當(dāng)scrollview在頂部并向下拖拽時(shí)淫痰,隱藏scrollview中本來的UIImageview,造一個(gè)一模一樣的用來移動(dòng)的imageview整份。獲取到scrollview的panGestureRecognizer的手指位置信息待错,移動(dòng)并縮放圖片,通知代理設(shè)置控制器背景透明度烈评。手指松開時(shí)火俄,根據(jù)手指瞬間方向判斷是否需要退出頁面,并執(zhí)行相應(yīng)操作讲冠。
難點(diǎn)解決
- 手勢綁定事件的方法在父類中瓜客,怎么實(shí)時(shí)監(jiān)控手勢發(fā)生并移動(dòng)了呢?怎么監(jiān)聽手勢結(jié)束呢竿开?
當(dāng)然是用到UIScrollview的代理方法谱仪。
/** scrollview正在滾動(dòng) */
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
/** scrollview即將結(jié)束拖拽 */
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset;
這里需要注意的是,如果圖片沒有屏幕大否彩,那么scrollview的contentSize是小于frame的疯攒,這個(gè)時(shí)候并不能拖拽,代理方法自然也不執(zhí)行列荔。需要設(shè)置scrollview的屬性:
/** 總是有彈簧效果 */
_mainScrollView.alwaysBounceVertical = YES;
_mainScrollView.alwaysBounceHorizontal = YES;//這是為了左右滑時(shí)能夠及時(shí)回調(diào)scrollViewDidScroll代理
注意左右的彈簧屬性也要設(shè)置敬尺,否則向下拖動(dòng)交互進(jìn)行中枚尼,如果用戶開始左右拖動(dòng),而mainScrollView不能左右拖動(dòng)砂吞,那代理方法不會(huì)執(zhí)行署恍,會(huì)造成圖片卡住不動(dòng)的感覺。
- 造跟著手指動(dòng)的imageView時(shí)呜舒,初始frame的計(jì)算
其實(shí)不算很難锭汛,但是要場景要考慮全面,因?yàn)樵谕蟿?dòng)時(shí)袭蝗,圖片可能已經(jīng)是放大狀態(tài)唤殴。
- (void)saveFrameBeginPan
{
imageWidthBeforeDrag = self.mainImageView.yy_width;//開始時(shí)的高
imageHeightBeforeDrag = self.mainImageView.yy_height;//開始時(shí)的寬
//計(jì)算圖片Y需要考慮到圖片此時(shí)的高,如果足夠高時(shí)到腥,交互發(fā)生時(shí)y一定是0
CGFloat imageBeginY = (imageHeightBeforeDrag < kMainScreenHeight) ? (kMainScreenHeight - imageHeightBeforeDrag) * 0.5 : 0.0;
imageYBeforeDrag = imageBeginY; //+ imageHeightBeforeDrag * 0.5;
//centerX需要考慮到offset
scrollOffsetX = self.mainScrollView.contentOffset.x;
CGFloat imageX = -scrollOffsetX;
imageCenterXBeforeDrag = imageX + imageWidthBeforeDrag * 0.5;
}
為什么是y值加centerX的值朵逝?
因?yàn)檫@樣圖片縮小的效果是往圖片最中間縮。
其它小細(xì)節(jié)
- 雙手捏合來縮放圖片時(shí)乡范,也會(huì)調(diào)用代理scrollViewDidScroll
解決:根據(jù)手勢的手指數(shù)>1判斷是否做交互配名,另外還可以設(shè)置一個(gè)變量來記錄是不是正在縮放。 - 向下拖拽時(shí)向左右滑動(dòng)會(huì)出現(xiàn)上一張/下一張圖片的邊緣
解決:拖拽時(shí)通知代理隱藏其它圖片晋辆,結(jié)束時(shí)顯示出來渠脉。 - 手勢拖動(dòng)時(shí),其實(shí)scrollview正在被拖動(dòng)瓶佳,里面的圖片也就正在移動(dòng)芋膘,圖片彈回去時(shí),位置就變化了霸饲,會(huì)有一個(gè)瞬移的感覺
解決:拖動(dòng)開始記錄下offset为朋,結(jié)束拖動(dòng)時(shí)賦值回去。 - 松開時(shí)怎么判斷是否返回
解決:在拖動(dòng)時(shí)記錄瞬間的方向厚脉,即如果當(dāng)前手勢點(diǎn)的y大于之前的习寸,解釋向下移動(dòng),松開就要退出頁面傻工。否則不退出頁面霞溪。甚至還可以加一個(gè),如果只像下拉了一丟丟中捆,就不退出頁面威鹿。 - 當(dāng)scrollview的offset.y小于0時(shí)才執(zhí)行交互代碼,那如果向下拉了轨香,不松手又向上拉,此時(shí)offset.y就大于0了幼东,那不就不執(zhí)行代碼了臂容,圖片不就卡住不動(dòng)了嗎科雳?
解決:設(shè)置變量來記錄是否正在下拉,拉動(dòng)開始是為yes脓杉,結(jié)束時(shí)為no糟秘,當(dāng)offset.y<0或者變量為yes,就執(zhí)行下拉球散。 - 小細(xì)節(jié)太多了尿赚。。蕉堰。凌净。
實(shí)戰(zhàn)效果
其它
- demo地址:https://github.com/YYProgrammer/YYPhotoBrowserLikeWX
- 項(xiàng)目中用到的動(dòng)畫用的pop框架,以及轉(zhuǎn)場動(dòng)畫的做法屋讶,在這里可以看到詳解:http://www.reibang.com/p/a65d3463f4bc