【问题标题】:iOS - Resize multiple views with touch-drag separatorsiOS - 使用触摸拖动分隔符调整多个视图的大小
【发布时间】:2016-03-01 15:07:56
【问题描述】:

如何使用分隔符调整视图大小?我正在尝试做的是类似于Instagram layout app。我希望能够通过拖动分隔视图的线来调整视图大小。

我已经调查过这个question。它类似于我想要完成的事情,我已经尝试了答案,但是如果有超过 2 个视图连接到分隔符,则它不起作用(如果有 3 个或更多视图,则每次分隔符移动时只有 2 个视图调整大小)。我试图更改代码,但我不知道该怎么做或代码的含义。

在我的应用中,我将获得 2-6 次观看。分隔符应该调整它旁边的所有视图的大小。

我的观点的一些例子:

我怎样才能做到这一点?我从哪里开始?

【问题讨论】:

    标签: ios objective-c


    【解决方案1】:

    有很多方法可以做到这一点,但像 Avinash 一样,我建议在各种“内容”UIView 对象之间创建一个“分隔视图”。然后你可以拖动它。不过,这里的技巧是,您可能希望分隔视图比狭窄的可见线更大,这样它不仅可以捕获分隔线上的触摸,还可以捕获靠近它的触摸。

    与您引用的其他答案不同,现在我建议您使用自动布局,以便您对用户手势所做的一切就是更新分隔视图的位置(例如更新分隔视图的顶部约束),以及然后所有其他视图将自动为您调整大小。我还建议对子视图的大小添加一个低优先级约束,以便在您第一次设置所有内容时以及在开始拖动分隔符之前很好地布置它们,但是当拖动的分隔符指示时它会优雅地失败相邻视图的大小必须改变。

    最后,虽然我们在历史上会使用手势识别器来处理类似的事情,但随着 iOS 9 中预测触摸的出现,我建议只实现 touchesBegantouchesMoved 等。使用预测触摸,你赢了'没有注意到模拟器或旧设备上的差异,但是当您在能够预测触摸的设备上运行它时(例如,iPad Pro 等新设备和其他新设备),您将获得响应更快的用户体验。

    所以水平分隔视图类可能如下所示。

    static CGFloat const kTotalHeight = 44;                               // the total height of the separator (including parts that are not visible
    static CGFloat const kVisibleHeight = 2;                              // the height of the visible portion of the separator
    static CGFloat const kMargin = (kTotalHeight - kVisibleHeight) / 2.0; // the height of the non-visible portions of the separator (i.e. above and below the visible portion)
    static CGFloat const kMinHeight = 10;                                 // the minimum height allowed for views above and below the separator
    
    /** Horizontal separator view
    
     @note This renders a separator view, but the view is larger than the visible separator
     line that you see on the device so that it can receive touches when the user starts 
     touching very near the visible separator. You always want to allow some margin when
     trying to touch something very narrow, such as a separator line.
     */
    
    @interface HorizontalSeparatorView : UIView
    
    @property (nonatomic, strong) NSLayoutConstraint *topConstraint;      // the constraint that dictates the vertical position of the separator
    @property (nonatomic, weak) UIView *firstView;                        // the view above the separator
    @property (nonatomic, weak) UIView *secondView;                       // the view below the separator
    
    // some properties used for handling the touches
    
    @property (nonatomic) CGFloat oldY;                                   // the position of the separator before the gesture started
    @property (nonatomic) CGPoint firstTouch;                             // the position where the drag gesture started
    
    @end
    
    @implementation HorizontalSeparatorView
    
    #pragma mark - Configuration
    
    /** Add a separator between views
    
     This creates the separator view; adds it to the view hierarchy; adds the constraint for height; 
     adds the constraints for leading/trailing with respect to its superview; and adds the constraints 
     the relation to the views above and below
    
     @param firstView  The UIView above the separator
     @param secondView The UIView below the separator
     @returns          The separator UIView
     */
    
    + (instancetype)addSeparatorBetweenView:(UIView *)firstView secondView:(UIView *)secondView {
        HorizontalSeparatorView *separator = [[self alloc] init];
        [firstView.superview addSubview:separator];
        separator.firstView = firstView;
        separator.secondView = secondView;
    
        [NSLayoutConstraint activateConstraints:@[
            [separator.heightAnchor constraintEqualToConstant:kTotalHeight],
            [separator.superview.leadingAnchor constraintEqualToAnchor:separator.leadingAnchor],
            [separator.superview.trailingAnchor constraintEqualToAnchor:separator.trailingAnchor],
            [firstView.bottomAnchor constraintEqualToAnchor:separator.topAnchor constant:kMargin],
            [secondView.topAnchor constraintEqualToAnchor:separator.bottomAnchor constant:-kMargin],
        ]];
    
        separator.topConstraint = [separator.topAnchor constraintEqualToAnchor:separator.superview.topAnchor constant:0]; // it doesn't matter what the constant is, because it hasn't been enabled
    
        return separator;
    }
    
    - (instancetype)init {
        self = [super init];
        if (self) {
            self.translatesAutoresizingMaskIntoConstraints = false;
            self.userInteractionEnabled = true;
            self.backgroundColor = [UIColor clearColor];
        }
        return self;
    }
    
    #pragma mark - Handle Touches
    
    // When it first receives touches, save (a) where the view currently is; and (b) where the touch started
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.oldY = self.frame.origin.y;
        self.firstTouch = [[touches anyObject] locationInView:self.superview];
        self.topConstraint.constant = self.oldY;
        self.topConstraint.active = true;
    }
    
    // When user drags finger, figure out what the new top constraint should be
    
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        UITouch *touch = [touches anyObject];
    
        // for more responsive UX, use predicted touches, if possible
    
        if ([UIEvent instancesRespondToSelector:@selector(predictedTouchesForTouch:)]) {
            UITouch *predictedTouch = [[event predictedTouchesForTouch:touch] lastObject];
            if (predictedTouch) {
                [self updateTopConstraintOnBasisOfTouch:predictedTouch];
                return;
            }
        }
    
        // if no predicted touch found, just use the touch provided
    
        [self updateTopConstraintOnBasisOfTouch:touch];
    }
    
    // When touches are done, reset constraint on the basis of the final touch,
    // (backing out any adjustment previously done with predicted touches, if any).
    
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self updateTopConstraintOnBasisOfTouch:[touches anyObject]];
    }
    
    /** Update top constraint of the separator view on the basis of a touch.
    
     This updates the top constraint of the horizontal separator (which moves the visible separator).
     Please note that this uses properties populated in touchesBegan, notably the `oldY` (where the
     separator was before the touches began) and `firstTouch` (where these touches began).
    
     @param touch    The touch that dictates to where the separator should be moved.
     */
    - (void)updateTopConstraintOnBasisOfTouch:(UITouch *)touch {
        // calculate where separator should be moved to
    
        CGFloat y = self.oldY + [touch locationInView:self.superview].y - self.firstTouch.y;
    
        // make sure the views above and below are not too small
    
        y = MAX(y, self.firstView.frame.origin.y + kMinHeight - kMargin);
        y = MIN(y, self.secondView.frame.origin.y + self.secondView.frame.size.height - (kMargin + kMinHeight));
    
        // set constraint
    
        self.topConstraint.constant = y;
    }
    
    #pragma mark - Drawing
    
    - (void)drawRect:(CGRect)rect {
        CGRect separatorRect = CGRectMake(0, kMargin, self.bounds.size.width, kVisibleHeight);
        UIBezierPath *path = [UIBezierPath bezierPathWithRect:separatorRect];
        [[UIColor blackColor] set];
        [path stroke];
        [path fill];
    }
    
    @end
    

    垂直分隔符可能看起来非常相似,但我会把这个练习留给你。

    不管怎样,你可以这样使用它:

    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        UIView *previousContentView = nil;
    
        for (NSInteger i = 0; i < 4; i++) {
            UIView *contentView = [self addRandomColoredView];
            [self.view.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor].active = true;
            [self.view.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor].active = true;
            if (previousContentView) {
                [HorizontalSeparatorView addSeparatorBetweenView:previousContentView secondView:contentView];
                NSLayoutConstraint *height = [contentView.heightAnchor constraintEqualToAnchor:previousContentView.heightAnchor];
                height.priority = 250;
                height.active = true;
            } else {
                [self.view.topAnchor constraintEqualToAnchor:contentView.topAnchor].active = true;
            }
            previousContentView = contentView;
        }
        [self.view.bottomAnchor constraintEqualToAnchor:previousContentView.bottomAnchor].active = true;
    }
    
    - (UIView *)addRandomColoredView {
        UIView *someView = [[UIView alloc] init];
        someView.translatesAutoresizingMaskIntoConstraints = false;
        someView.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256)/255.0 green:arc4random_uniform(256)/255.0 blue:arc4random_uniform(256)/255.0 alpha:1.0];
        [self.view addSubview:someView];
    
        return someView;
    }
    
    @end
    

    这会产生如下内容:

    正如我所提到的,垂直分隔符看起来非常相似。如果您有带有垂直和水平分隔符的复杂视图,您可能希望拥有不可见的容器视图来隔离垂直和水平视图。例如,考虑您的一个例子:

    这可能包含两个视图,它们跨越设备的整个宽度,带有一个水平分隔符,然后顶部视图本身将具有两个带有一个垂直分隔符的子视图,底部视图将具有三个带有两个子视图的子视图垂直分隔符。


    这里有很多东西,所以在你尝试推断上面的例子来处理(a)垂直分隔符之前;然后(b)views-within-views 模式,确保你真的理解上面的例子是如何工作的。这并不是一个通用的解决方案,而只是为了说明您可能采用的模式。但希望这能说明基本思想。

    【讨论】:

    • 非常感谢罗伯!我会尽量理解这一点!
    • 嗨,罗伯。我试图创建垂直分隔符。 [firstView.rightAnchor constraintEqualToAnchor:separator.leftAnchor constant:kMargin], [secondView.leftAnchor constraintEqualToAnchor:separator.rightAnchor constant:kMargin], 但我的 ViewController 中没有显示任何内容。我在哪里可以向您展示我的整个代码?
    • 嗨,罗伯。我在这里用我的垂直分隔符代码问了一个新问题:stackoverflow.com/questions/35775231/… 你能指导我做错什么吗?我尝试将 contentView 右锚点连接到分隔符左锚点,并将 contentView 左锚点连接到分隔符右锚点,但是当我运行代码时。它显示了一个空白屏幕。
    • 谢谢,这对我帮助很大。我也在尝试相同的方法,但另外,我想执行与布局应用程序相同的拖放功能,所以你在更新中这样做了吗?
    【解决方案2】:

    我已将@JULIIncognito 的 Swift 类更新为 Swift 4,添加了拖动指示器并修复了一些拼写错误。

    SeparatorView

    只需导入您的项目并像这样使用它:

    SeparatorView.addSeparatorBetweenViews(separatorType: .horizontal, primaryView: view1, secondaryView: view2, parentView: self.view)
    

    这是它的样子(顶部是 MapView,底部是 TableView):

    【讨论】:

      【解决方案3】:

      基于 Rob 的解决方案,我为水平和垂直分隔符视图创建了 Swift 类: https://gist.github.com/JULI-ya/1a7c293b022207bb427caa3bbb9d3ed8

      只有两个带有分隔符的内部视图的代码,因为我的想法是将彼此放在一起以创建此自定义布局。它看起来像视图的二叉树结构。

      【讨论】:

      • 如何在视图控制器中进行设置?例如,这个布局:i.stack.imgur.com/EA6uY.png
      • @cannyboy 图像上有 3 个分隔符视图。彼此内。其余的逻辑是基于简单视图的布局构建。
      • 虽然这段代码运行了,但屏幕上没有任何显示。当我传递视图的属性时,屏幕保持白色。我检查了调试视图层次结构中显示的内容,它只显示了一个 UIView 和主 UIWindow。没有分隔符或指示器。有什么我想念的吗?
      【解决方案4】:

      使用 UIPanGestureRecognizers。为每个视图添加一个识别器。在gestureRecognizerShouldBegin: 方法中,如果手势位置非常靠近边缘,则返回YES(使用手势的locationInView:view 方法)。然后在手势的动作方法中(在手势的initWithTarget: action: 中指定)你会像这样处理你的动作:

      -(void)viewPan:(UIPanGestureRecognizer *)sender
      
          switch (sender.state) {
      
          case UIGestureRecognizerStateBegan: {
      
              //determine the second view based on gesture's locationInView: 
              //for instance if close to bottom, the second view is the one under the current.
      
          }
      
          case UIGestureRecognizerStateChanged: {
               //change the frames of the current and the second view based on sender's translationInView:
          }
      
          ...
      }
      

      【讨论】:

        【解决方案5】:

        据我所知,我们可以使用 UIGestureRecognizer 和自动布局

        1. 使用 UIView 作为行分隔符。
        2. 将平移手势识别器添加到分隔线视图。
        3. 使用UIView.animatewithDuration()
        PanGestureRecognizer 处理委托协议方法中的视图移动

        最重要的是,不要忘记在 Attribute Inspector 中为所有行分隔符视图设置/检查 UserInteration Enabled。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-02-13
          • 1970-01-01
          • 2010-10-14
          • 1970-01-01
          • 2014-04-17
          • 2015-07-19
          相关资源
          最近更新 更多