【YYTextView 自增高編輯 && 類IQKeyboardManager功能】

【最終效果預覽】 git

實現效果預覽

【先決條件】 本文全部代碼針對的具體場景爲信息發佈頁,效果圖中 紅色 -- 標題,青色 -- 摘要, 藍色 -- 圖片描述, 其中圖片可添加多張,在此不贅述。(表情切換、輸入功能正常可用,因爲涉及到具體項目信息,未展現。) 全部輸入框,均爲YYTextView。github

【具體需求】 類Facebook 、微博頭條文章的發佈頁,這個需求對於安卓端來講好像相對簡單,但對iOS來講稍微有點困難。bash

【解決思路】 思路一:全局YYtextView 來實現,意思是整個發佈頁底層就是一個YYTextView,這樣的好處是文本編輯等等體驗都是無縫的。但YY有一個潛在問題,當內容渲染達到必定高度就會出現白板問題。 因爲項目發佈頁實際上圖片可能達到數百張,故放棄。app

思路二:分析了微博的實現,最終和他們相似底層採用UITableView,Cell上放置YYTextView 來作。佈局

【疑難點】動畫

  1. UITableView 輸入過程當中若是採用reload方法來變高,會崩潰。
  2. Cell上文本增高時,UITableView 上移邏輯。
  3. 光標起始定位問題

【最終實現】 可參考相關源碼 ,修改了YYTextView源碼實現了相似IQKeyboardManager 的自動調整功能(固然是必定條件下)。ui

【控制器須要處理的代碼】spa

/// 注意相關的YYTextView實例 scrollEnabled 要設置爲 NO !!!
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    //開啓調整功能
    [YYTextView setAutoCursorEnable:YES];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [YYTextView setAutoCursorEnable:NO];
}
複製代碼

【UITableView 須要處理的代碼】設計

//自增高 更新YYTextView 高度
//觸發源方法
- (void)textViewDidChange:(YYTextView *)textView {
  ....
  CGFloat fltTextHeight = textView.textLayout.textBoundingSize.height;
  textView.scrollEnabled = NO; //必須設置爲NO

  //這裏動畫的做用是抵消,YYTextView 內部動畫 防止視覺上的跳動。
  [UIView animateWithDuration:0.25 animations:^{
            textView.height = fltTextHeight;
   } completion:^(BOOL finished) {

  }];
  ....
  ....  
  
  CGSize layoutSize = originalSize;
  layoutSize.height = topInset + bottomInset + fltTextHeight;

  //獲取底層TableView 
  UITableView tableView = ....; // 假設,具體怎樣設計代碼自行處理。
  //重要的部分 
  [tableView beginUpdates];
  //假設這裏 textView 是放置在UITableView 的HeaderView上
  tableView.tableHeaderSize = layoutSize;
  [tableView endUpdates];
  ....

}
複製代碼

【YYTextView的改動】 主要修改部分rest

- (void)_scrollRangeToVisible:(YYTextRange *)range {
    if (!range) return;
    //獲取頂層ScrollView
    UIScrollView *scTop = [self _findTopScrollView];
    //從內部佈局容器中獲取 光標位置
    CGRect rect = [_innerLayout rectForRange:range];
    if (CGRectIsNull(rect)) return;
    //轉換區域
    rect = [self _convertRectFromLayout:rect];
    //轉換區域到頂層SC
    CGRect rectTop = [_containerView convertRect:rect toView:scTop];
    
    //轉換區域到內部文本容器
    rect = [_containerView convertRect:rect toView:self];

    if (rect.size.width < 1) {
        rect.size.width = 1;
        rectTop.size.width = 1;
    }
    if (rect.size.height < 1) {
        rect.size.height = 1;
        rectTop.size.height = 1;
    }
    
    CGFloat extend = 3;
    //是否修改內間距
    BOOL insetModified = NO;

    //鍵盤管理器
    YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager];
    
    //須要移動頂層容器的狀況
    if (!self.scrollEnabled && [YYTextView autoCursorEnable]) {
        ///**添加自動調整外部頂層 UITableView 偏移,用來實現和IQKeyboard相似的功能
        //滾動鎖定狀態下
        //鍵盤彈起狀況下
        CGRect topBounds = scTop.bounds;
        topBounds.origin = CGPointZero;
        //保存原始間距數據
        if(!_isAutoCursorEnable){
            _isAutoCursorEnable = YES;
            _originalTopContentInset = scTop.contentInset;
            _originalTopScrollIndicatorInsets = scTop.scrollIndicatorInsets;
        }
        
        CGRect kbTopRect = [mgr convertRect:mgr.keyboardFrame toView:scTop];
        kbTopRect.origin.y -= _extraAccessoryViewHeight;
        kbTopRect.size.height += _extraAccessoryViewHeight;
        //修正鍵盤位置
        kbTopRect.origin.x -= scTop.contentOffset.x;
        kbTopRect.origin.y -= scTop.contentOffset.y;
        //區域交集
        CGRect inter = CGRectIntersection(topBounds, kbTopRect);
        UIEdgeInsets newTopInset = UIEdgeInsetsZero;
        newTopInset.bottom = inter.size.height + extend;
        
        UIViewAnimationOptions curve;
        if (kiOS7Later) {
            curve = 7 << 16;
        } else {
            curve = UIViewAnimationOptionCurveEaseInOut;
        }
        [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | curve animations:^{
    
            [scTop setContentInset:newTopInset];
            [scTop setScrollIndicatorInsets:newTopInset];
            [scTop scrollRectToVisible:CGRectInset(rectTop, -extend, -extend) animated:NO];
            
        } completion:NULL];
        
        return;
    }
    
    if (mgr.keyboardVisible && self.window && self.superview && self.isFirstResponder && !_verticalForm) {
        //鍵盤彈起狀況下
        CGRect bounds = self.bounds;
        bounds.origin = CGPointZero;
        CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self];
        kbRect.origin.y -= _extraAccessoryViewHeight;
        kbRect.size.height += _extraAccessoryViewHeight;
        //修正鍵盤位置
        kbRect.origin.x -= self.contentOffset.x;
        kbRect.origin.y -= self.contentOffset.y;
        //區域交集
        CGRect inter = CGRectIntersection(bounds, kbRect);
        if (!CGRectIsNull(inter) && inter.size.height > 1 && inter.size.width > extend) { // self is covered by keyboard
            if (CGRectGetMinY(inter) > CGRectGetMinY(bounds)) { // keyboard below self.top
                //獲取當前內間距數據
                UIEdgeInsets originalContentInset = self.contentInset;
                UIEdgeInsets originalScrollIndicatorInsets = self.scrollIndicatorInsets;
                
                //默認值爲NO
                if (_insetModifiedByKeyboard) {
                    //從上一次偏移中獲取內間距數據
                    originalContentInset = _originalContentInset;
                    originalScrollIndicatorInsets = _originalScrollIndicatorInsets;
                }
                
                if (originalContentInset.bottom < inter.size.height + extend) {
                    //當前光標被鍵盤遮擋
                    insetModified = YES;
                    if (!_insetModifiedByKeyboard) {
                        //第一次 保存原始內間距等設置
                        _insetModifiedByKeyboard = YES;
                        _originalContentInset = self.contentInset;
                        _originalScrollIndicatorInsets = self.scrollIndicatorInsets;
                    }
                    
                    CGFloat fltDiffBottom = CGRectGetMaxY(bounds) - CGRectGetMaxY(inter);
                    
                    //內間距更新
                    UIEdgeInsets newInset = originalContentInset;
                    UIEdgeInsets newIndicatorInsets = originalScrollIndicatorInsets;
                    
                    //固定爲鍵盤高度
                    newInset.bottom = inter.size.height + extend + fltDiffBottom;
                    newIndicatorInsets.bottom = newInset.bottom;

                    UIViewAnimationOptions curve;
                    if (kiOS7Later) {
                        curve = 7 << 16;
                    } else {
                        curve = UIViewAnimationOptionCurveEaseInOut;
                    }
                    [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | curve animations:^{
                        
                        [super setContentInset:newInset];
                        [super setScrollIndicatorInsets:newIndicatorInsets];
                        [self scrollRectToVisible:CGRectInset(rect, -extend, -extend) animated:NO];

                    } completion:NULL];
                }
            }
        }
    }
    if (!insetModified) {
        [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut animations:^{
            
            [self _restoreInsetsAnimated:NO];
            [self scrollRectToVisible:CGRectInset(rect, -extend, -extend) animated:NO];
        } completion:NULL];
    }
}

/// Restore contents insets if modified by keyboard.
- (void)_restoreInsetsAnimated:(BOOL)animated {
    if (_insetModifiedByKeyboard) {
        _insetModifiedByKeyboard = NO;
        if (animated) {
            [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut  animations:^{
                [super setContentInset:_originalContentInset];
                [super setScrollIndicatorInsets:_originalScrollIndicatorInsets];
            } completion:NULL];
        } else {
            [super setContentInset:_originalContentInset];
            [super setScrollIndicatorInsets:_originalScrollIndicatorInsets];
        }
    }
    
    if ([YYTextView autoCursorEnable]) {
        
        UIScrollView *scTop = [self _findTopScrollView];
        
        if (animated) {
            [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut  animations:^{
                //還原頂層容器間距
                [scTop setContentInset:_originalTopContentInset];
                [scTop setScrollIndicatorInsets:_originalTopScrollIndicatorInsets];
                
            } completion:NULL];
        } else {
            //還原頂層容器間距
            [scTop setContentInset:_originalTopContentInset];
            [scTop setScrollIndicatorInsets:_originalTopScrollIndicatorInsets];
        }
    }
    
}

/// Find top scrollView, Implement function like IQKeyboard.
- (UIScrollView *)_findTopScrollView {
    UIScrollView *topScrollView = nil;
    UIView *viewTemp = self.superview;
    while (viewTemp && ![viewTemp isKindOfClass:[UIScrollView class]]) {
        viewTemp = viewTemp.superview;
    }
    ///correct the result
    if ([viewTemp isKindOfClass: NSClassFromString(@"UITableViewWrapperView")]) {
        viewTemp = viewTemp.superview;
    }
    
    topScrollView = (UIScrollView *)viewTemp;
    
    return topScrollView;
}
複製代碼

最後但願能幫到有須要的人,由於是直接在工做項目中修改因此暫無Demo。