Dưới đây là những gì bạn cần biết để loại bỏ nhấp nháy:
Như Caleb loại chỉ ra, Core Animation xoay lớp của bạn để trục X tích cực của nó nằm dọc theo tang của con đường của bạn. Bạn cần làm cho định hướng "tự nhiên" của hình ảnh của bạn hoạt động với điều đó. Vì vậy, giả sử đó là một tàu vũ trụ màu xanh lục trong hình ảnh ví dụ của bạn, bạn cần tàu vũ trụ để trỏ đến bên phải khi nó không có vòng quay áp dụng cho nó.
Đặt biến đổi bao gồm vòng quay cản trở vòng xoay được áp dụng bởi `kCAAnimationRotateAuto '. Bạn cần xóa vòng xoay khỏi biến đổi của mình trước khi áp dụng hoạt ảnh.
Tất nhiên điều đó có nghĩa là bạn cần đăng ký lại chuyển đổi khi hoạt ảnh hoàn tất. Và tất nhiên bạn muốn làm điều đó mà không nhìn thấy bất kỳ nhấp nháy trong sự xuất hiện của hình ảnh. Đó không phải là khó khăn, nhưng có một số nước sốt bí mật liên quan, mà tôi giải thích dưới đây.
Bạn có lẽ muốn tàu vũ trụ của bạn bắt đầu chỉ dọc theo đường tiếp tuyến của đường dẫn, ngay cả khi tàu vũ trụ đang ngồi vẫn chưa được làm động. Nếu hình ảnh tàu vũ trụ của bạn đang trỏ sang phải, nhưng con đường của bạn tăng lên, thì bạn cần phải thiết lập biến đổi của hình ảnh để bao gồm một vòng quay 90 °. Nhưng có lẽ bạn không muốn hardcode mà quay - thay vào đó bạn muốn nhìn vào con đường và tìm ra bắt đầu của nó tiếp tuyến.
Tôi sẽ hiển thị một số mã quan trọng tại đây. Bạn có thể tìm thấy my test project on github. Bạn có thể tìm thấy một số sử dụng trong việc tải xuống và dùng thử. Chỉ cần nhấn vào "tàu vũ trụ" màu xanh lục để xem hoạt ảnh.
Vì vậy, trong dự án thử nghiệm của mình, tôi đã kết nối UIImageView
với một hành động có tên animate:
. Khi bạn chạm vào nó, hình ảnh di chuyển dọc theo một nửa của hình 8 và tăng gấp đôi kích thước. Khi bạn chạm vào nó một lần nữa, hình ảnh di chuyển dọc theo nửa kia của hình 8 (trở lại vị trí bắt đầu), và trở về kích thước ban đầu của nó. Cả hai hình động sử dụng kCAAnimationRotateAuto
, vì vậy hình ảnh chỉ dọc theo ốp của đường dẫn.
Đây là sự khởi đầu của animate:
, nơi tôi tìm ra những gì con đường, quy mô và mục đích rõ hình ảnh nên kết thúc tại địa chỉ:
- (IBAction)animate:(id)sender {
UIImageView* theImage = self.imageView;
UIBezierPath *path = _isReset ? _path0 : _path1;
CGFloat newScale = 3 - _currentScale;
CGPoint destination = [path currentPoint];
Vì vậy, điều đầu tiên tôi cần làm là loại bỏ bất kỳ luân chuyển từ của hình ảnh chuyển đổi, vì như tôi đã đề cập, nó sẽ gây trở ngại cho kCAAnimationRotateAuto
:
// Strip off the image's rotation, because it interferes with `kCAAnimationRotateAuto`.
theImage.transform = CGAffineTransformMakeScale(_currentScale, _currentScale);
Tiếp theo, tôi đi vào một khối UIView
hoạt hình để hệ thống sẽ áp dụng hoạt ảnh cho xem hình ảnh:
[UIView animateWithDuration:3 animations:^{
tôi tạo ra các hình ảnh động keyframe cho vị trí và thiết lập một vài thuộc tính của nó:
// Prepare my own keypath animation for the layer position.
// The layer position is the same as the view center.
CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
positionAnimation.path = path.CGPath;
positionAnimation.rotationMode = kCAAnimationRotateAuto;
Tiếp theo là bí mật sốt để ngăn ngừa nhấp nháy ở phần cuối của phim hoạt hình. Nhớ lại rằng hình động làm không tác dụng các thuộc tính của "lớp mô hình" mà bạn đính kèm chúng vào (theImage.layer
trong trường hợp này). Thay vào đó, họ cập nhật các thuộc tính của "lớp trình bày", phản ánh những gì thực sự trên màn hình.
Vì vậy, trước tiên, tôi đặt removedOnCompletion
thành NO
cho hoạt ảnh của khung hình chính. Điều này có nghĩa là hoạt ảnh sẽ được gắn liền với lớp mô hình khi hoạt ảnh hoàn tất, có nghĩa là tôi có thể truy cập lớp trình bày. Tôi nhận được biến đổi từ lớp trình bày, xóa hoạt ảnh và áp dụng biến đổi cho lớp mô hình. Vì tất cả điều này xảy ra trên luồng chính, những thay đổi thuộc tính này tất cả xảy ra trong một chu kỳ làm mới màn hình, vì vậy không có nhấp nháy.
positionAnimation.removedOnCompletion = NO;
[CATransaction setCompletionBlock:^{
CGAffineTransform finalTransform = [theImage.layer.presentationLayer affineTransform];
[theImage.layer removeAnimationForKey:positionAnimation.keyPath];
theImage.transform = finalTransform;
}];
Bây giờ tôi đã thiết lập khối hoàn thành, tôi thực sự có thể thay đổi thuộc tính chế độ xem. Hệ thống sẽ tự động đính kèm hình động vào lớp khi tôi làm điều này.
// UIView will add animations for both of these changes.
theImage.transform = CGAffineTransformMakeScale(newScale, newScale);
theImage.center = destination;
tôi sao chép một số tính chất quan trọng từ những hình ảnh động vị trí tự động gia tăng cho hoạt hình keyframe của tôi:
// Copy properties from UIView's animation.
CAAnimation *autoAnimation = [theImage.layer animationForKey:positionAnimation.keyPath];
positionAnimation.duration = autoAnimation.duration;
positionAnimation.fillMode = autoAnimation.fillMode;
và cuối cùng tôi thay thế các hình ảnh động vị trí tự động gia tăng với các hình ảnh động keyframe:
// Replace UIView's animation with my animation.
[theImage.layer addAnimation:positionAnimation forKey:positionAnimation.keyPath];
}];
Cuối cùng, tôi cập nhật biến mẫu của mình để phản ánh thay đổi đối với chế độ xem hình ảnh:
_currentScale = newScale;
_isReset = !_isReset;
}
Đó là cách hiển thị hình ảnh không bị nhấp nháy.
Và bây giờ, như Steve Jobs sẽ nói, Một điều cuối cùng. Khi tôi tải chế độ xem, tôi cần đặt biến đổi của chế độ xem hình ảnh sao cho nó được xoay theo điểm dọc theo đường tiếp tuyến của đường dẫn đầu tiên mà tôi sẽ sử dụng để tạo hiệu ứng đó. Tôi làm điều đó trong một phương thức có tên reset
:
- (void)reset {
self.imageView.center = _path1.currentPoint;
self.imageView.transform = CGAffineTransformMakeRotation(startRadiansForPath(_path0));
_currentScale = 1;
_isReset = YES;
}
Tất nhiên, chút khéo léo ẩn trong đó startRadiansForPath
chức năng. Nó thực sự không phải là khó. Tôi sử dụng hàm CGPathApply
để xử lý các phần tử của đường đi, chọn ra hai điểm đầu tiên thực sự tạo thành một đường con, và tôi tính toán góc của đường được hình thành bởi hai điểm đó. (Một phần đường cong là một đường thẳng đứng bậc hai hoặc khối bezier, và những đường thẳng này có đặc tính là tang tại điểm đầu tiên của đường spline là đường từ điểm đầu tiên đến điểm kiểm soát kế tiếp.)
I ' m chỉ cần kết xuất mã ở đây mà không cần giải thích, cho hậu thế:
typedef struct {
CGPoint p0;
CGPoint p1;
CGPoint firstPointOfCurrentSubpath;
CGPoint currentPoint;
BOOL p0p1AreSet : 1;
} PathState;
static inline void updateStateWithMoveElement(PathState *state, CGPathElement const *element) {
state->currentPoint = element->points[0];
state->firstPointOfCurrentSubpath = state->currentPoint;
}
static inline void updateStateWithPoints(PathState *state, CGPoint p1, CGPoint currentPoint) {
if (!state->p0p1AreSet) {
state->p0 = state->currentPoint;
state->p1 = p1;
state->p0p1AreSet = YES;
}
state->currentPoint = currentPoint;
}
static inline void updateStateWithPointsElement(PathState *state, CGPathElement const *element, int newCurrentPointIndex) {
updateStateWithPoints(state, element->points[0], element->points[newCurrentPointIndex]);
}
static void updateStateWithCloseElement(PathState *state, CGPathElement const *element) {
updateStateWithPoints(state, state->firstPointOfCurrentSubpath, state->firstPointOfCurrentSubpath);
}
static void updateState(void *info, CGPathElement const *element) {
PathState *state = info;
switch (element->type) {
case kCGPathElementMoveToPoint: return updateStateWithMoveElement(state, element);
case kCGPathElementAddLineToPoint: return updateStateWithPointsElement(state, element, 0);
case kCGPathElementAddQuadCurveToPoint: return updateStateWithPointsElement(state, element, 1);
case kCGPathElementAddCurveToPoint: return updateStateWithPointsElement(state, element, 2);
case kCGPathElementCloseSubpath: return updateStateWithCloseElement(state, element);
}
}
CGFloat startRadiansForPath(UIBezierPath *path) {
PathState state;
memset(&state, 0, sizeof state);
CGPathApply(path.CGPath, &state, updateState);
return atan2f(state.p1.y - state.p0.y, state.p1.x - state.p0.x);
}
Điều này đã giúp tôi rất nhiều! Cảm ơn! – RyanG