167

: この質問が出されてから、事態は進行しています。最近の概要については、こちらを参照してください。


自動レイアウトの前は、フレームを保存し、アンカー ポイントを設定し、フレームを復元することで、ビューを移動せずにビューのレイヤーのアンカー ポイントを変更できました。

自動レイアウトの世界では、フレームを設定することはもうありませんが、制約は、ビューの位置を調整して元の位置に戻すタスクに対応していないようです。制約をハックしてビューを再配置できますが、回転またはその他のサイズ変更イベントでは、これらは再び無効になります。

次の素晴らしいアイデアは、「無効なレイアウト属性 (左と幅) の組み合わせ」を作成するため機能しません。

layerView.layer.anchorPoint = CGPointMake(1.0, 0.5);
// Some other size-related constraints here which all work fine...
[self.view addConstraint:
    [NSLayoutConstraint constraintWithItem:layerView
                                 attribute:NSLayoutAttributeLeft
                                 relatedBy:NSLayoutRelationEqual 
                                    toItem:layerView 
                                 attribute:NSLayoutAttributeWidth 
                                multiplier:0.5 
                                  constant:20.0]];

ここでの私の意図はlayerView、調整されたアンカー ポイントを持つビューの左端を、その幅の半分に 20 を加えた値 (スーパービューの左端から挿入したい距離) に設定することでした。

自動レイアウトでレイアウトされたビューで、ビューの位置を変更せずにアンカーポイントを変更することはできますか? ハードコーディングされた値を使用して、回転ごとに制約を編集する必要がありますか? ないことを願っています。

ビューに変換を適用したときに正しい視覚効果が得られるように、アンカー ポイントを変更する必要があります。

4

12 に答える 12

366

[編集:警告:以降の議論全体は、iOS 8 によって時代遅れになるか、少なくとも大幅に緩和される可能性があります。これにより、ビューの変換が適用されたときにレイアウトをトリガーするという間違いを犯すことはなくなります。]

Autolayout と View Transform

Autolayout は、ビューの変換ではまったくうまく機能しません。私が知る限り、その理由は、(デフォルトの恒等変換以外の) 変換を持つビューのフレームをいじってはいけないということですが、それはまさに autolayout が行うことです。自動レイアウトが機能する方法はlayoutSubviews、実行時にすべての制約を通過し、それに応じてすべてのビューのフレームを設定することです。

つまり、制約は魔法ではありません。それらは単なるやることリストです。layoutSubviewsやることリストが完成する場所です。そして、フレームを設定することでそれを行います。

これをバグと見なさざるを得ません。この変換をビューに適用すると:

v.transform = CGAffineTransformMakeScale(0.5,0.5);

ビューの中心が以前と同じ場所に表示され、半分のサイズで表示されることを期待しています。しかし、その制約によっては、それは私が見ているものとはまったく異なる場合があります.

[実際には、ここで 2 つ目の驚きがあります。ビューに変換を適用すると、すぐにレイアウトがトリガーされます。これは私には別のバグのようです。あるいは、それが最初のバグの核心なのかもしれません。私が期待するのは、少なくともレイアウト時間まで変換を回避できることです。たとえば、レイアウト時間までフレームアニメーションを回避できるのと同じように、デバイスが回転します。しかし、実際にはレイアウト時間は即時であり、これは間違っているようです。]

解決策 1: 制約なし

現在の解決策の 1 つは、半永久的な変換をビューに適用する場合 (そして、何らかの方法で一時的に振るだけでなく)、ビューに影響を与えるすべての制約を削除することです。残念ながら、自動レイアウトは引き続き行われ、ビューを配置する場所を指定する制約がないため、通常、これによりビューが画面から消えてしまいます。したがって、制約を削除するだけでなく、ビューtranslatesAutoresizingMaskIntoConstraintsを YES に設定しました。ビューは古い方法で機能するようになり、自動レイアウトの影響を受けなくなりました。(明らかに自動レイアウト影響を受けますが、暗黙的な自動サイズ変更マスクの制約により、その動作は自動レイアウトの前と同じようになります。)

解決策 2: 適切な制約のみを使用する

それが少し極端に思える場合、別の解決策は、意図した変換で正しく機能するように制約を設定することです。たとえば、ビューのサイズがその内部の固定幅と高さだけで決まり、純粋にその中心で配置されている場合、スケール変換は期待どおりに機能します。このコードでは、サブビュー ( otherView) の既存の制約を削除し、これらの 4 つの制約に置き換えて、幅と高さを固定し、純粋に中心で固定します。その後、スケール変換が機能します。

NSMutableArray* cons = [NSMutableArray array];
for (NSLayoutConstraint* con in self.view.constraints)
    if (con.firstItem == self.otherView || con.secondItem == self.otherView)
        [cons addObject:con];

[self.view removeConstraints:cons];
[self.otherView removeConstraints:self.otherView.constraints];
[self.view addConstraint:
 [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeCenterX relatedBy:0 toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:self.otherView.center.x]];
[self.view addConstraint:
 [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeCenterY relatedBy:0 toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:self.otherView.center.y]];
[self.otherView addConstraint:
 [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeWidth relatedBy:0 toItem:nil attribute:0 multiplier:1 constant:self.otherView.bounds.size.width]];
[self.otherView addConstraint:
 [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeHeight relatedBy:0 toItem:nil attribute:0 multiplier:1 constant:self.otherView.bounds.size.height]];

要するに、ビューのフレームに影響を与える制約がない場合、自動レイアウトはビューのフレームに触れないということです。これは、変換が関係しているときに求めているものです。

解決策 3: サブビューを使用する

上記の両方のソリューションの問題は、ビューを配置するための制約の利点が失われることです。そこで、それを解決するソリューションを紹介します。ホストとしてのみ機能する非表示のビューから開始し、コンストレイントを使用して配置します。その中に、実際のビューをサブビューとして配置します。制約を使用してサブビューをホスト ビュー内に配置しますが、それらの制約を、変換を適用したときに反撃しない制約に制限します。

以下に図を示します。

ここに画像の説明を入力

白いビューはホスト ビューです。透明で見えないふりをすることになっています。赤いビューはそのサブビューで、その中心をホスト ビューの中心に固定することによって配置されます。これで、赤のビューを問題なく中心を中心にスケーリングおよび回転できます。実際、図はそのようにしたことを示しています。

self.otherView.transform = CGAffineTransformScale(self.otherView.transform, 0.5, 0.5);
self.otherView.transform = CGAffineTransformRotate(self.otherView.transform, M_PI/8.0);

その間、ホスト ビューの制約により、デバイスを回転させてもホスト ビューが適切な場所に保持されます。

解決策 4: 代わりにレイヤー変換を使用する

ビュー変換の代わりに、レイヤー変換を使用します。これはレイアウトをトリガーしないため、制約との即時の競合を引き起こしません。

たとえば、次の単純な「ドキドキ」ビュー アニメーションは、自動レイアウトで壊れる可能性があります。

[UIView animateWithDuration:0.3 delay:0
                    options:UIViewAnimationOptionAutoreverse
                 animations:^{
    v.transform = CGAffineTransformMakeScale(1.1, 1.1);
} completion:^(BOOL finished) {
    v.transform = CGAffineTransformIdentity;
}];

最終的にビューのサイズに変更はありませんでしたが、単に設定するtransformとレイアウトが発生し、制約によってビューが飛び回る可能性があります。(これはバグのように感じますか?) しかし、Core Animation で同じことを行うと (CABasicAnimation を使用し、アニメーションをビューのレイヤーに適用します)、レイアウトは発生せず、正常に動作します。

CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:@"transform"];
ba.autoreverses = YES;
ba.duration = 0.3;
ba.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.1, 1.1, 1)];
[v.layer addAnimation:ba forKey:nil];
于 2012-12-31T20:20:31.553 に答える
41

私は似たような問題を抱えていて、Apple の Autolayout チームから戻ってきたばかりです。彼らは Container View Approach マットの提案を使用することを提案しますが、UIView のサブクラスを作成して layoutSubviews を上書きし、そこにカスタム レイアウト コードを適用します

ヘッダー ファイルはそのように見えるため、選択したサブビューを Interface Builder から直接リンクできます。

#import <UIKit/UIKit.h>

@interface BugFixContainerView : UIView
@property(nonatomic,strong) IBOutlet UIImageView *knobImageView;
@end

m ファイルは、次のような特別なコードを適用します。

#import "BugFixContainerView.h"

@implementation BugFixContainerView
- (void)layoutSubviews
{
    static CGPoint fixCenter = {0};
    [super layoutSubviews];
    if (CGPointEqualToPoint(fixCenter, CGPointZero)) {
        fixCenter = [self.knobImageView center];
    } else {
        self.knobImageView.center = fixCenter;
    }
}
@end

ご覧のとおり、最初に呼び出されたときにビューの中心点を取得し、その後の呼び出しでその位置を再利用して、それに応じてビューを配置します。これは、[super layoutSubviews] の後に実行されるという意味で Autolayout コードを上書きします。自動レイアウトコードが含まれています。

このように、Autolayout を回避する必要はなくなりましたが、デフォルトの動作が適切でなくなった場合は、独自の autolayout を作成できます。もちろん、その例よりもはるかに複雑なものを適用することもできますが、私のアプリはポートレート モードしか使用できないため、必要なのはこれだけでした。

于 2013-01-02T08:52:46.270 に答える
9

簡単な方法を見つけました。また、iOS 8 および iOS 9 で動作します。

フレームベースのレイアウトを使用する場合は、anchorPoint を調整します。

let oldFrame = layerView.frame
layerView.layer.anchorPoint = newAnchorPoint
layerView.frame = oldFrame

ビューのアンカーを自動レイアウトで調整する場合、同じことを行いますが、制約の方法で行います。anchorPoint が (0.5, 0.5) から (1, 0.5) に変わると、layerView はビュー幅の半分の長さだけ左に移動するため、これを補正する必要があります。

質問の制約がわかりません。そのため、定数を持つ superView centerX に対して centerX 制約を追加すると仮定します: layerView.centerX = superView.centerX + 定数

layerView.layer.anchorPoint = CGPoint(1, 0.5)
let centerXConstraint = .....
centerXConstraint.constant = centerXConstraint.constant + layerView.bounds.size.width/2
于 2015-10-09T06:08:49.637 に答える
4

自動レイアウトを使用している場合、最終的には自動レイアウトが独自のレイアウトを計算するときに設定した位置の値を上書きするため、手動で位置を設定することが長期的にどのように機能するかわかりません。

むしろ、アンカーポイントの設定によって生じる変更を補うために、レイアウトの制約自体を変更する必要があります。次の関数は、変換されていないビューに対してそれを行います。

/**
  Set the anchorPoint of view without changing is perceived position.

 @param view view whose anchorPoint we will mutate
 @param anchorPoint new anchorPoint of the view in unit coords (e.g., {0.5,1.0})
 @param xConstraint an NSLayoutConstraint whose constant property adjust's view x.center
 @param yConstraint an NSLayoutConstraint whose constant property adjust's view y.center

  As multiple constraints can contribute to determining a view's center, the user of this
 function must specify which constraint they want modified in order to compensate for the
 modification in anchorPoint
 */
void SetViewAnchorPointMotionlesslyUpdatingConstraints(UIView * view,CGPoint anchorPoint,
                                                       NSLayoutConstraint * xConstraint,
                                                       NSLayoutConstraint * yConstraint)
{
  // assert: old and new anchorPoint are in view's unit coords
  CGPoint const oldAnchorPoint = view.layer.anchorPoint;
  CGPoint const newAnchorPoint = anchorPoint;

  // Calculate anchorPoints in view's absolute coords
  CGPoint const oldPoint = CGPointMake(view.bounds.size.width * oldAnchorPoint.x,
                                 view.bounds.size.height * oldAnchorPoint.y);
  CGPoint const newPoint = CGPointMake(view.bounds.size.width * newAnchorPoint.x,
                                 view.bounds.size.height * newAnchorPoint.y);

  // Calculate the delta between the anchorPoints
  CGPoint const delta = CGPointMake(newPoint.x-oldPoint.x, newPoint.y-oldPoint.y);

  // get the x & y constraints constants which were contributing to the current
  // view's position, and whose constant properties we will tweak to adjust its position
  CGFloat const oldXConstraintConstant = xConstraint.constant;
  CGFloat const oldYConstraintConstant = yConstraint.constant;

  // calculate new values for the x & y constraints, from the delta in anchorPoint
  // when autolayout recalculates the layout from the modified constraints,
  // it will set a new view.center that compensates for the affect of the anchorPoint
  CGFloat const newXConstraintConstant = oldXConstraintConstant + delta.x;
  CGFloat const newYConstraintConstant = oldYConstraintConstant + delta.y;

  view.layer.anchorPoint = newAnchorPoint;
  xConstraint.constant = newXConstraintConstant;
  yConstraint.constant = newYConstraintConstant;
  [view setNeedsLayout];
}

通常、アンカーポイントを変更する唯一の理由は変換を設定することであるため、これがおそらくあなたが望んでいたすべてではないことを認めます。これには、transform プロパティ自体によって発生する可能性のあるすべてのフレームの変更を反映するようにレイアウトの制約を更新する、より複雑な関数が必要になります。変換はフレームに対して多くのことを行うことができるため、これは注意が必要です。スケーリングまたは回転変換によりフレームが大きくなるため、幅や高さの制約などを更新する必要があります.

一時的なアニメーションにのみ変換を使用している場合は、自動レイアウトによって、飛行中のアニメーションが純粋に一時的な制約違反を表す画像を表示することを妨げるとは思わないため、上記で十分な場合があります。

于 2012-11-26T12:37:42.017 に答える
3

tl:dr:制約の 1 つにアウトレットを作成して、それを削除して再度追加できるようにすることができます。


新しいプロジェクトを作成し、中央に固定サイズのビューを追加しました。制約を下の図に示します。

私が考えることができる最小の例の制約。

次に、回転するビューと中心 x 配置制約用のアウトレットを追加しました。

@property (weak, nonatomic) IBOutlet UIView *rotatingView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *xAlignmentContstraint;

後でviewDidAppear新しいアンカーポイントを計算します

UIView *view = self.rotatingView;

CGPoint rotationPoint = // The point I'm rotating around... (only X differs)
CGPoint anchorPoint = CGPointMake((rotationPoint.x-CGRectGetMinX(view.frame))/CGRectGetWidth(view.frame),
                                  (rotationPoint.y-CGRectGetMinY(view.frame))/CGRectGetHeight(view.bounds));

CGFloat xCenterDifference = rotationPoint.x-CGRectGetMidX(view.frame);

view.layer.anchorPoint = anchorPoint;

次に、アウトレットがある制約を削除し、オフセットされた新しい制約を作成して、再度追加します。その後、制約を更新する必要があることを、制約が変更されたビューに伝えます。

[self.view removeConstraint:self.xAlignmentContstraint];
self.xAlignmentContstraint = [NSLayoutConstraint constraintWithItem:self.rotatingView
                                                          attribute:NSLayoutAttributeCenterX
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:self.view
                                                          attribute:NSLayoutAttributeCenterX
                                                         multiplier:1.0
                                                           constant:xDiff];
[self.view addConstraint:self.xAlignmentContstraint];
[self.view needsUpdateConstraints];

最後に、回転アニメーションを回転ビューに追加します。

CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotate.toValue = @(-M_PI_2);
rotate.autoreverses = YES;
rotate.repeatCount = INFINITY;
rotate.duration = 1.0;
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 

[view.layer addAnimation:rotate forKey:@"myRotationAnimation"];

回転レイヤーは、デバイスを回転させたり、その他の方法で制約を更新したりしても、中央に配置されたままのように見えます (そうあるべきです)。新しい制約と変更されたアンカー ポイントは、互いに視覚的に相殺されます。

于 2012-12-31T16:02:26.800 に答える
1

私の現在の解決策は、レイヤーの位置を手動で調整することviewDidLayoutSubviewsです。このコードはlayoutSubviewsビュー サブクラスにも使用できますが、私の場合、ビューはビュー コントローラー内のトップレベル ビューであるため、UIView サブクラスを作成する必要はありませんでした。

あまりにも多くの努力のように思えるので、他の答えは大歓迎です。

-(void)viewDidLayoutSubviews
{
    for (UIView *view in self.view.subviews)
    {
        CGPoint anchorPoint = view.layer.anchorPoint;
        // We're only interested in views with a non-standard anchor point
        if (!CGPointEqualToPoint(CGPointMake(0.5, 0.5),anchorPoint))
        {
            CGFloat xDifference = anchorPoint.x - 0.5;
            CGFloat yDifference = anchorPoint.y - 0.5;
            CGPoint currentPosition = view.layer.position;

            // Use transforms if we can, otherwise manually calculate the frame change
            // Assuming a transform is in use since we are changing the anchor point. 
            if (CATransform3DIsAffine(view.layer.transform))
            {
                CGAffineTransform current = CATransform3DGetAffineTransform(view.layer.transform);
                CGAffineTransform invert = CGAffineTransformInvert(current);
                currentPosition = CGPointApplyAffineTransform(currentPosition, invert);
                currentPosition.x += (view.bounds.size.width * xDifference);
                currentPosition.y += (view.bounds.size.height * yDifference);
                currentPosition = CGPointApplyAffineTransform(currentPosition, current);
            }
            else
            {
                CGFloat transformXRatio = view.bounds.size.width / view.frame.size.width;

                if (xDifference < 0)
                    transformXRatio = 1.0/transformXRatio;

                CGFloat transformYRatio = view.bounds.size.height / view.frame.size.height;
                if (yDifference < 0)
                    transformYRatio = 1.0/transformYRatio;

                currentPosition.x += (view.bounds.size.width * xDifference) * transformXRatio;
                currentPosition.y += (view.bounds.size.height * yDifference) * transformYRatio;
            }
            view.layer.position = currentPosition;
        }

    }
}
于 2012-10-18T10:14:15.093 に答える
1

私のマットの答えに影響を与えたので、別のアプローチを試すことにしました。制約が適切に適用されたコンテナー ビューを使用できます。変更されたアンカー ポイントを含むビューは、コンテナー ビュー内に配置できます。これは、昔のように自動サイズ変更マスクと明示的なフレーム設定を使用して行います。

とにかく私の状況では、それは御馳走になります。ビューは、viewDidLoad で次のように設定されます。

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    UIView *redView = [UIView new];
    redView.translatesAutoresizingMaskIntoConstraints = NO;
    redView.backgroundColor = [UIColor redColor];
    [self.view addSubview:redView];

    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[redView]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(redView)]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[redView]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(redView)]];
    self.redView = redView;

    UIView *greenView = [UIView new];
    greenView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
    greenView.layer.anchorPoint = CGPointMake(1.0, 0.5);
    greenView.frame = redView.bounds;
    greenView.backgroundColor = [UIColor greenColor];
    [redView addSubview:greenView];
    self.greenView = greenView;

    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = 0.005;
    self.redView.layer.sublayerTransform = perspective;
}

緑のビューの自動サイズ変更マスクのため、この時点で赤のビューのフレームがゼロであることは問題ではありません。

アクション メソッドに回転変換を追加した結果、次のようになりました。

ここに画像の説明を入力

デバイスの回転中に失われたように見えたので、これを viewDidLayoutSubviews メソッドに追加しました。

-(void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    CATransform3D transform = self.greenView.layer.transform;
    self.greenView.layer.transform = CATransform3DIdentity;
    self.greenView.frame = self.redView.bounds;
    self.greenView.layer.transform = transform;
    [CATransaction commit];

}
于 2013-01-01T14:36:52.490 に答える
0

これは大きなトピックであり、すべてのコメントを読んだわけではありませんが、同じ問題に直面していました。

自動レイアウトでXIBからのビューがありました。そして、その変換プロパティを更新したかったのです。ビューをコンテナー ビューに埋め込んでも問題は解決しません。これは、自動レイアウトがコンテナー ビューで奇妙な動作をしていたためです。そのため、2 番目のコンテナー ビューを追加して、自分のビューを含み、それに変換を適用するコンテナー ビューを含めました。

于 2014-12-02T11:38:07.450 に答える
0

tl;dr アンカーポイントを (0, 0) に変更したとしましょう。アンカーポイントが左上になりました。自動レイアウトでcenterという単語が表示されるときはいつでも、 top-leftと考える必要があります。

anchorPoint を調整すると、AutoLayout のセマンティクスが変更されるだけです。自動レイアウトは、アンカーポイントに干渉することも、その逆もありません。これが分からないと大変なことになります


例:

図 A.アンカー ポイントの変更なし

#Before changing anchor point to top-left
view.size == superview.size
view.center == superview.center

図 B.左上に変更されたアンカー ポイント

view.layer.anchorPoint = CGPointMake(0, 0)
view.size == superview.size
view.center == superview.topLeft                <----- L0-0K, center is now top-left

図 A と図 B はまったく同じに見えます。何も変わっていません。センターが何を指すかの定義が変わっただけです。

于 2015-02-09T18:55:31.513 に答える
0

その方法で自動レイアウトの目的を無効にしていると思います。幅と右端はスーパービューに依存するとおっしゃいましたが、その考え方に沿って制約を追加してみませんか?

アンカーポイント/変換パラダイムを失い、試してください:

[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:layerView
                             attribute:NSLayoutAttributeRight
                             relatedBy:NSLayoutRelationEqual 
                                toItem:self.view 
                             attribute:NSLayoutAttributeWidth 
                            multiplier:1.0f
                              constant:-somePadding]];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:layerView
                             attribute:NSLayoutAttributeWidth
                             relatedBy:NSLayoutRelationEqual 
                                toItem:someViewWeDependTheWidthOn
                             attribute:NSLayoutAttributeWidth 
                            multiplier:0.5f // because you want it to be half of someViewWeDependTheWidthOn
                              constant:-20.0f]]; // your 20pt offset from the left

NSLayoutAttributeRight制約は とまったく同じ意味anchorPoint = CGPointMake(1.0, 0.5)であり、制約は前のNSLayoutAttributeWidthコードの とほぼ同等NSLayoutAttributeLeftです。

于 2012-10-19T00:22:38.760 に答える