ios - كيف أقوم بضبط نقطة الربط لـ CALayer ، عندما يتم استخدام Auto Layout؟




autolayout (8)

[EDIT: تحذير: من المحتمل أن تكون المناقشة التي تبعت ذلك قد عفا عليها الزمن أو على الأقل قد تم تخفيفها بشكل كبير بواسطة نظام التشغيل iOS 8 ، الذي لم يعد من الممكن ارتكاب خطأ في تشغيل التخطيط في الوقت الذي يتم فيه تطبيق تحويل العرض.]

Autolayout مقابل عرض التحولات

لا يعمل Autolayout على الإطلاق مع تحويلات المشاهدة. السبب ، بقدر ما أستطيع أن أميز ، هو أنك لست من المفترض أن تفسد إطار عرض يحتوي على تحويل (بخلاف تحويل الهوية الافتراضي) - ولكن هذا بالضبط ما يفعله autolayout. الطريقة التي يعمل بها autolayout هي أن في layoutSubviews في وقت التشغيل يأتي وقت التشغيل عبر جميع القيود ووضع الإطارات لجميع وجهات النظر وفقا لذلك.

بمعنى آخر ، القيود ليست سحرية. هم مجرد قائمة المهام. layoutSubviews حيث يتم layoutSubviews . ويفعل ذلك عن طريق وضع الإطارات.

لا استطيع المساعدة فيما يتعلق بهذا الخطأ. إذا قمت بتطبيق هذا التحويل على طريقة عرض:

v.transform = CGAffineTransformMakeScale(0.5,0.5);

أتوقع أن ترى المنظر يظهر مع مركزه في نفس المكان كما كان من قبل وبنصف الحجم. ولكن اعتمادًا على قيودها ، قد لا يكون هذا ما أراه على الإطلاق.

[في الواقع ، هناك مفاجأة أخرى هنا: يؤدي تطبيق تحويل إلى عرض إلى تشغيل التخطيط على الفور. يبدو لي هذا خطأ آخر. أو ربما يكون قلب الحشرة الأولى. ما أتوقعه هو أن أكون قادرًا على الابتعاد عن عملية تحويل على الأقل حتى وقت التصميم ، على سبيل المثال ، يتم تدوير الجهاز - تمامًا كما أتمكن من التخلص من الرسوم المتحركة للإطار حتى وقت التخطيط. لكن في الواقع ، وقت التخطيط فوري ، والذي يبدو خاطئًا.]

الحل 1: لا توجد قيود

أحد الحلول الحالية هو ، إذا كنت سأطبّق تحويلًا شبه دائم إلى عرض (وليس مجرد تحريكه مؤقتًا بطريقة ما) ، لإزالة جميع القيود التي تؤثر فيه. لسوء الحظ ، يؤدي هذا عادةً إلى اختفاء العرض من الشاشة ، نظرًا لأن autolayout لا يزال يحدث ، والآن لا توجد أية قيود لإخبارنا بمكان وضع العرض. لذا بالإضافة إلى إزالة القيود ، أقوم translatesAutoresizingMaskIntoConstraints عرض العرضتقويم الحوائط إلى القيود على YES. يعمل العرض الآن بالطريقة القديمة ، ولا يتأثر بشكل فعال بواسطة autolayout. (يتأثر ذلك بالتعطيل الذاتي ، من الواضح ، لكن قيود قناع التأويل الضمني تسبب سلوكه كما كان قبل autolayout.)

الحل 2: استخدام القيود المناسبة فقط

إذا كان هذا يبدو قاسيًا بعض الشيء ، فإن الحل الآخر هو تعيين القيود للعمل بشكل صحيح مع التحويل المقصود. إذا كان حجم العرض محصوراً من خلال العرض والارتفاع الثابتين الداخليين ، وتم وضعه بمنتصفه تماماً ، على سبيل المثال ، سيعمل تحويلي الخاص بالمقياس كما أتوقع. في هذا الكود ، أقوم بإزالة القيود الموجودة على عرض otherView ( otherView ) otherView بالقيود الأربعة هذه ، مع إعطائها عرضًا ثابتًا otherView . بعد ذلك ، يعمل تحويل النطاق الخاص بي:

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]];

النتيجة هي أنه إذا لم يكن لديك أي قيود تؤثر على إطار العرض ، فلن يلمس autolayout إطار العرض - وهو ما تفعله فقط عندما يتم تضمين التحويل.

الحل 3: استخدم Subview

المشكلة مع كل من الحلول أعلاه هي أننا نفقد فوائد القيود لوضع وجهة نظرنا. إذن هذا هو الحل الذي يحل ذلك. ابدأ برؤية غير مرئية وظيفتها الوحيدة كمضيف ، واستخدم القيود لوضعها. في الداخل ، ضع العرض الحقيقي على أنه عرض فرعي. استخدم القيود لوضع موضع العرض الفرعي داخل عرض المضيف ، ولكن مع تقييد هذه القيود على القيود التي لن ترد عند تطبيق تحويل.

فيما يلي توضيح:

العرض الأبيض هو عرض المضيف؛ من المفترض أن تتظاهر بأنه شفاف وبالتالي غير مرئي. العرض الأحمر هو عرضه الفرعي ، ويتم وضعه من خلال تثبيت مركزه على مركز عرض المضيف. يمكننا الآن توسيع نطاق العرض الأحمر وتناوبه حول مركزه بدون أي مشكلة ، وفي الواقع يوضح الرسم التوضيحي أننا قمنا بذلك:

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

وفي الوقت نفسه ، فإن القيود المفروضة على عرض المضيف تحتفظ به في المكان المناسب بينما نقوم بتدوير الجهاز.

الحل 4: استخدام تحويل الطبقة بدلاً من ذلك

بدلاً من عرض التحويلات ، استخدم تحويلات الطبقات ، التي لا تؤدي إلى تشغيل التخطيط ، وبالتالي لا تتسبب في حدوث تعارض فوري مع القيود.

على سبيل المثال ، قد تنكسر حركة عرض "throb" البسيطة هذه تحت autolayout:

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

على الرغم من أنه في النهاية لم يكن هناك تغيير في حجم العرض ، إلا أن تحديد transform يتسبب في حدوث تخطيط ، ويمكن أن تؤدي القيود إلى تقليب المشهد. (هل هذا يبدو وكأنه خطأ أم ماذا؟) ولكن إذا فعلنا نفس الشيء مع الرسوم المتحركة الأساسية (باستخدام 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];

ملاحظة : سارت الأمور منذ طرح السؤال. انظر here للحصول على نظرة عامة حديثة جيدة.

قبل التخطيط التلقائي ، يمكنك تغيير نقطة الربط لطبقة عرض دون نقل العرض عن طريق تخزين الإطار ، وتعيين نقطة الربط ، واستعادة الإطار.

في عالم التصميم التلقائي ، لم نقم بتعيين الإطارات بعد الآن ، ولكن لا يبدو أن القيود تكمن في مهمة تعديل موضع العرض مرة أخرى إلى المكان الذي نريده. يمكنك اختراق القيود لإعادة تحديد موضع العرض الخاص بك ، ولكن في حالة التدوير أو أحداث تغيير الحجم الأخرى ، تصبح غير صالحة مرة أخرى.

لا تعمل الفكرة الساطعة التالية أثناء إنشاء "سمات الاقتران غير الصحيحة للتخطيط (اليسار والعرض)":

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 (المسافة التي أريدها من الحافة اليسرى للمشاهدة الفائقة).

هل من الممكن تغيير نقطة الربط ، دون تغيير موقع العرض ، في طريقة العرض التي تم تخطيطها باستخدام التخطيط التلقائي؟ هل أحتاج إلى استخدام قيم hardcoded وتحرير القيد في كل دوران؟ أتمنى ذلك.

أحتاج إلى تغيير نقطة الربط بحيث عندما أتقدم بتحويل إلى العرض ، أحصل على التأثير المرئي الصحيح.


tl؛ dr دعنا نقول أنك غيرت نقطة الربط إلى (0، 0). نقطة الربط الآن أعلى اليسار. في أي وقت ترى كلمة المركز في التخطيط التلقائي ، يجب أن تفكر من أعلى اليسار .

عندما تقوم بتعديل أنكويربوينت ، فإنك تقوم فقط بتغيير دلالات AutoLayout. لن يتداخل التخطيط التلقائي مع نظام التثبيت لديك ولا العكس. إذا لم تفهم هذا ، فسوف يكون لديك وقت سيء .

مثال:

الشكل أ لا تعديلات نقطة الارتساء

#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

الشكل (أ) والشكل (ب) تبدو متطابقة تماماً. لا شيء تغير. مجرد تعريف ما يشير المركز للتغيير.


أعتقد أنك تتغلب على الغرض من autolayout بتلك الطريقة. لقد ذكرت أن العرض والحافة اليمنى يعتمدان على superview ، فلماذا لا نكتفي بإضافة قيود على هذا الخط من التفكير؟

تفقد نموذج anchorPoint / transform وجرب:

[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 يكافئ تقريباً NSLayoutAttributeWidth الخاص بالرمز السابق الخاص بك.


ألهمتني هذه الأسئلة والأجوبة على حل مشكلاتي الخاصة مع Autolayout وقياسها ، ولكن مع عروض التمرير. أنا خلقت مثالا على حل بلدي على جيثب:

https://github.com/hansdesmedt/AutoLayout-scrollview-scale

هذا مثال على UIScrollView مع الاستدعاء المخصص بالكامل في AutoLayout وقابلة للتحجيم (CATransform3DMakeScale) مع الضغط لفترة طويلة والاستفادة للتكبير. iOS 6 و 7 متوافق.


إنه موضوع كبير ولم أقرأ كل التعليقات ولكنني واجهت نفس المشكلة.

كان لدي رأي من XIB مع autolayout. وأردت تحديث خاصية التحويل الخاصة به. لا يؤدي تضمين العرض في عرض حاوية إلى حل مشكلتي نظرًا لأن عملية autolayout كانت تعمل بشكل غريب على عرض الحاوية. ولهذا السبب أضفت للتو عرض الحاوية الثاني لاحتواء عرض الحاوية الذي يحتوي على وجهة نظري وكنت أطبق التحويلات عليه.


الحل الحالي هو ضبط موضع الطبقة يدويًا في viewDidLayoutSubviews . يمكن استخدام هذا الرمز أيضًا في layoutSubviews لفئة فرعية من 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;
        }

    }
}

مستوحاة إجابة مات ، قررت أن أجرب نهجًا مختلفًا. يمكن استخدام عرض الحاوية ، مع وجود قيود مطبقة بشكل مناسب. يمكن بعد ذلك وضع العرض مع نقطة الربط المعدلة داخل عرض الحاوية ، باستخدام أقنعة التوحيد والترتيب الصريح للإطار كما هو الحال في الأيام القديمة السيئة.

يعمل علاج ، لحالتي على أي حال. يتم إعداد المشاهدات هنا في 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];

}

tl: dr: يمكنك إنشاء منفذ لأحد القيود بحيث يمكن إزالته وإضافته مرة أخرى.

لقد أنشأت مشروعًا جديدًا وأضفت عرضًا بحجم ثابت في المركز. تظهر القيود في الصورة أدناه.

بعد ذلك ، أضفت مخرجًا للعرض الذي سيتم تدويره ولقيد المحاذاة إلى الوسط 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"];

تبدو الطبقة الدوارة وكأنها تبقى مركزًا (والتي يجب أن تكون) حتى عند تدوير الجهاز أو جعله يقوم بتحديث القيود. القيد الجديد ونقطة الربط تغير بصريا كل منهما الآخر.







autolayout