Перетащите и измените размер UIView с помощью Touches, рабочего решения, запрашивающего обратную связь - PullRequest
3 голосов
/ 28 января 2012

В моем приложении у меня есть вид, который я хочу изменить, используя касание двумя пальцами аналогично, но не близко к тому, для чего распознаватель жестов пинч обеспечивает обнаружение.

Идея похожа на то, что вы делали бы нана рабочем столе, схватив мышкой один из четырех углов, за исключением того, что мне нужен более дружественный к сенсорам интерфейс, в котором величина, на которую уменьшается или увеличивается каждый такой угол, не зависит ни от горизонтали, ни от вертикали.так как масштабный коэффициент пинча одинаков как для X, так и для Y, а это не то, что я хочу.

Мне нужно обнаружить два таких пальца и изменить размер / переместить вид как естественный.

И у меня все получилось.

Идея, которую я использовал (в дополнение к половинному постоянству объектов UITouch ...), заключалась в том, чтобы считать последний движущийся палец «целью» с предыдущим движущимся пальцем.как «якорь».

Затем я вычисляю вектор от якоря до цели, который указывает на один из четырех углов (это всегдаys делает это даже в случае оси), позволяя мне увеличивать / уменьшать ширину / высоту вида одновременно с перемещением или без его начала, давая эффект, что вы также можете изменять размеры верхней и левой сторон (также необходимо изменить исходную точку)в качестве только ширины / высоты (исходная точка не указана) или их комбинации.

Чтобы определить, сколько мне нужно сжать / увеличить / сместить, я использую разницу между текущей целевой точкой и предыдущей целевой точкой.Другими словами, вектор используется для определения того, на какой угол я указываю, и, таким образом, на какой «квадрант» работает касание, что позволяет мне выбирать, какие из x, y, ширину или высоту изменить, и целевой ток /предыдущая позиция говорит мне, сколько.

Есть две проблемы, с которыми я могу смириться, но мне интересно, прошел ли кто-нибудь лишнюю милю.

Пользовательский опыт отличныйза исключением немного неестественного ощущения, которое возникает в результате изменения размера верхнего правого угла с помощью жеста, когда оба пальца находятся в нижнем левом углу.Это делает именно то, что диктует движение пальца, но немного похоже на «жуткое действие на расстоянии».Может мне просто нужно к этому привыкнуть?Я не могу придумать, как изменить жест, чтобы добиться чего-то более естественного.

Математика.Вроде некрасиво.Я хотел использовать аффинное преобразование, но не смог понять, как применить его к своей проблеме, поэтому я прибег к старому триггеру арксинус / арккосин, а затем «включил» направление вектора, чтобы определить, какой «квадрант» (некоторой гипотетической единичной окружности, относящейся только к относительному положению якоря и цели, независимо от того, где они находятся на виде, - следовательно, проблема № 1 -).

итак, сводка вопросов:

  • Существует ли лучший, более удобный для пользователя подход, который позволил бы сделать эффект перетаскивания / изменения размера более совместимым с положением пальцев в представлении?
  • Аффинное преобразование сделает код чище?как?

Код.

A: упаковка UITouches

@interface UITouchWrapper : NSObject
@property (assign, nonatomic) CGPoint   centerOffset ;
@property (assign, nonatomic) UITouch * touch ;
@end
@implementation UITouchWrapper
@synthesize centerOffset ;
@synthesize touch ;
- (void) dealloc {
    ::NSLog(@"letting go of %@", self.touch) ;
}
@end

B.Обработка UITouch

@property (strong, nonatomic) NSMutableArray *              touchesWrapper ;

@synthesize touchesWrapper ;

- (UITouchWrapper *) wrapperForTouch: (UITouch *) touch {
    for (UITouchWrapper * w in self.touchesWrapper) {
        if (w.touch == touch) {
            return w ;
        }
    }

    UITouchWrapper * w = [[UITouchWrapper alloc] init] ;
    w.touch = touch ;
    [self.touchesWrapper addObject:w] ;
    return w ;
}

- (void) releaseWrapper: (UITouchWrapper *) wrapper {
    [self.touchesWrapper removeObject:wrapper] ;
}

- (NSUInteger) wrapperCount {
    return [self.touchesWrapper count] ;
}

C: начало касания

- (void) touchesBegan:(NSSet *) touches withEvent:(UIEvent *)event {
    // prime (possibly) our touch references. Touch events are unrelated ...
    for (UITouch * touch in [touches allObjects]) {
        // created on the fly if required
        UITouchWrapper * w = [self wrapperForTouch:touch] ;
        CGPoint p = [touch locationInView:[self superview]] ;
        p.x -= self.center.x ;
        p.y -= self.center.y ;
        w.centerOffset = p ;
    }
}

D: поиск «другой» точки (привязка)

- (UITouch *) anchorTouchFor: (UITouch *) touch {

    NSTimeInterval mostRecent = 0.0f ;
    UITouch * anchor = nil ;

    for (UITouchWrapper * w in touchesWrapper) {
        if (w.touch == touch) {
            continue ;
        }

        if (mostRecent < w.touch.timestamp) {
            mostRecent = w.touch.timestamp ;
            anchor = w.touch ;
        }
    }

    return anchor ;
}

E: обнаружение сопротивления (= перемещение в одно касание)

- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *) event {

    CGRect frame = self.frame ;

    for (UITouch * touch in [touches allObjects]) {
        UITouchWrapper * w = [self wrapperForTouch:touch] ;
        if ([self wrapperCount] == 1) {
            // that's a drag. w.touch and touch MUST agree
            CGPoint movePoint = [touch locationInView:[self superview]] ;

            CGPoint center = self.center ;
            center.x = movePoint.x - w.centerOffset.x ;
            center.y = movePoint.y - w.centerOffset.y ;
            self.center = center ;

            CGPoint p = movePoint ;
            p.x -= self.center.x ;
            p.y -= self.center.y ;
            w.centerOffset = p ;

            [self setNeedsDisplay] ;
// ...
       }
    }
}

F: вычисление угла [0 .. 2 пи] векторного якоря: касание

- (float) angleBetween: (UITouch *) anchor andTouch: (UITouch *) touch {
    // the coordinate sysem is flipped along the Y-axis...

    CGPoint a = [anchor locationInView:[self superview]] ;
    CGPoint t = [touch locationInView:[self superview]] ;

    // swap a and t to compensate for the flipped coordinate system;
    CGPoint d = CGPointMake(t.x-a.x, a.y-t.y) ;
    float distance = sqrtf(d.x * d.x + d.y * d.y) ;
    float cosa = (t.x - a.x) / distance ;
    float sina = (a.y - t.y) / distance ;

    float rc = ::acosf(cosa) ;
    float rs = ::asinf(sina) ;

    return rs >= 0.0f ? rc : (2.0f * M_PI) - rc ;
}

G: обработка изменения размера:

- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *) event {

    CGRect frame = self.frame ;
    // ...

    // That's a resize. We need to determine the direction of the
    // move. It is given by the vector made of this touch and the other 
    // touch. But if we have more than 2 touches, we use the one whose
    // time stamp is closest to this touch.

    UITouch * anchor = [self anchorTouchFor:touch] ;

    // don't do anything if we cannot find an anchor
    if (anchor == nil)  return ;

    CGPoint oldLoc = [touch previousLocationInView:[self superview]] ;
    CGPoint newLoc = [touch locationInView:[self superview]] ;

    CGPoint p = newLoc ;
    p.x -= self.center.x ;
    p.y -= self.center.y ;
    w.centerOffset = p ;

    CGFloat dx = newLoc.x - oldLoc.x ;
    CGFloat dy = newLoc.y - oldLoc.y ;

    float angle = [self angleBetween:anchor andTouch:touch] ;

    if (angle >= M_PI + M_PI_2) {   // 270 .. 360 bottom right
        frame.size.width += dx ;                
        frame.size.height += dy ;
    } else if (angle >= M_PI) {     // 180 .. 270 bottom left
        frame.size.width -= dx ;                
        frame.size.height += dy ;
        frame.origin.x += dx ;
    } else if (angle >= M_PI_2) {   //  90 .. 180 top left
        frame.size.width -= dx ;
        frame.origin.x += dx ;
        frame.size.height -= dy ;
        frame.origin.y += dy ;
    } else {                        //   0 ..  90 top right
        frame.size.width += dx ;
        frame.size.height -= dy ;
        frame.origin.y += dy ;
    }
    // ...
    self.frame = frame ;
    [self setNeedsLayout] ;
    [self setNeedsDisplay] ;

H: очистка при касанияхВыбрано / касанияОтменено

for (UITouch * touch in [touches allObjects]) {
    UITouchWrapper * w = [self wrapperForTouch:touch] ;
    if (w.touch == touch) {
        [self releaseWrapper:w] ;
    }
}
...