UIScrollView的慣性,彈跳和UIKit動態(tài)綁定

本篇翻譯至----UIScrollView's Inertia, Bouncing and Rubber-Banding with UIKit Dynamics

用UIKit動畫制作動畫邊界

使用UIKit Dynamics進行動畫處理的對象必須符合以下UIDynamicItem協(xié)議:

@protocol UIDynamicItem <NSObject>

@property (nonatomic, readwrite) CGPoint center;
@property (nonatomic, readonly) CGRect bounds;
@property (nonatomic, readwrite) CGAffineTransform transform;

@end

Dynamics使用centertransform屬性來移動項目的內(nèi)部算法和bounds屬性來計算沖突。因此,我們不能直接在滾動視圖上使用動畫來為其動畫boundsUIDynamicItem是一個協(xié)議,所以我們可以創(chuàng)建一個簡單的舊的NSObject子類符合它:
譯者注:UIKit Dynamics動畫必須綁定一個符合UIDynamicItem協(xié)議的一個對象

@interface CSCDynamicItem : NSObject <UIDynamicItem>

@property (nonatomic, readwrite) CGPoint center;
@property (nonatomic, readonly) CGRect bounds;
@property (nonatomic, readwrite) CGAffineTransform transform;

@end

@implementation CSCDynamicItem
- (instancetype)init {
    self = [super init];

    if (self) {
        // Sets non-zero `bounds`, because otherwise Dynamics throws an exception.
        _bounds = CGRectMake(0, 0, 1, 1);
    }

    return self;
}
@end

并使用這個類來驅(qū)動滾動視圖的更改bounds。為了簡單起見,我們假定動態(tài)項目center映射到滾動視圖bounds.origin。有幾種方法來綁定這些值:

  1. 我們可以將自己注冊為動態(tài)項目center關鍵路徑的觀察者,并更新bounds其值的變化。
  2. 我們可以將滾動視圖實例傳遞給動態(tài)項并從內(nèi)部更新其邊界setCenter:。
  3. 我們可以利用UIDynamicBehavior包含一個action屬性的事實,描述如下:

您要在動態(tài)動畫中執(zhí)行的塊。動態(tài)動畫師會在每個動畫步驟中調(diào)用動作塊。

我們選擇最后一個,因為這是最簡潔的,我認為它最適合這種情況。這里是代碼:

behavior.action = ^{
    CGRect bounds = weakSelf.bounds;
    bounds.origin = weakSelf.dynamicItem.center;
    weakSelf.bounds = bounds;
};

慣性滾動

我們現(xiàn)在準備添加一個慣性滾動。有兩件事情:1)當用戶在平移后將手指從屏幕上抬起時,滾動視圖應該繼續(xù)以相同的速度矢量滾動,2)滾動應該隨著時間減慢。瀏覽內(nèi)置行為的文檔時,我們很快注意到這一句UIDynamicItemBehavior描述:

動態(tài)項目行為的一個顯著和常見的用途是將動態(tài)項目的速度賦予與用戶手勢的結束速度匹配。

這個類非常靈活,它支持線性和角度運動。我們將使用從手勢識別器中抓取的速度–addLinearVelocity:forItem:推動動態(tài)項目。增加慣性是改變resistance值的一個簡單問題。下面是這個行為的完整設置:

self.dynamicItem.center = self.bounds.origin;
UIDynamicItemBehavior *decelerationBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.dynamicItem]];
[decelerationBehavior addLinearVelocity:velocity forItem:self.dynamicItem];
decelerationBehavior.resistance = 2.0;

__weak typeof(self) weakSelf = self;
decelerationBehavior.action = ^{
    CGRect bounds = weakSelf.bounds;
    bounds.origin = weakSelf.dynamicItem.center;
    weakSelf.bounds = bounds;
};

[self.animator addBehavior:decelerationBehavior];

你也可以在GitHub的這個階段看到整個文件。

彈性

彈性是最簡單的部分,它實際上根本不使用動態(tài)。我們只需要根據(jù)以下等式改變平移范圍:

f(x, d, c) = (x * d * c) / (d + c * x)

where,
x – distance from the edge
c – constant (UIScrollView uses 0.55)
d – dimension, either width or height

這是如何工作的:


上下滾動.gif

我們將使用UIAttachmentBehavior彈跳效果。這里的想法很簡單:當bounds.origin穿過可見區(qū)域(派生自contentSize)時,我們計算錨點,這是用戶最終的位置

CGPoint maxBoundsOrigin = CGPointMake(self.contentSize.width - bounds.size.width,
                                      self.contentSize.height - bounds.size.height);
CGPoint target = bounds.origin;
if (outsideBoundsMinimum) {
    target.x = fmin(maxBoundsOrigin.x, fmax(target.x, 0.0));
    target.y = fmin(maxBoundsOrigin.y, fmax(target.y, 0.0));
} else if (outsideBoundsMaximum) {
    target.x = fmax(0, fmin(target.x, maxBoundsOrigin.x));
    target.y = fmax(0, fmin(target.y, maxBoundsOrigin.y));
}

并附加到它的行為:

UIAttachmentBehavior *springBehavior = [[UIAttachmentBehavior alloc] initWithItem:self.dynamicItem attachedToAnchor:target];
// Has to be equal to zero, because otherwise the bounds.origin wouldn't exactly match the target's position.
springBehavior.length = 0;
// These two values were chosen by trial and error.
springBehavior.damping = 1;
springBehavior.frequency = 2;

[self.animator addBehavior:springBehavior];

該方法的其余部分主要是樣板,你可以在GitHub上看到它。

一切看起來很棒,直到我們嘗試滾動視圖,雙向啟用滾動。如果平移手勢具有非零的垂直和水平速度,則在bounds.origin接近以下位置時會出現(xiàn)難看的振蕩target

彈性.gif

發(fā)生這種情況是因為附件行為不能簡單地模擬沿線的動畫。它可以受到其他行為的影響,在我們的情況下decelerationBahavior,導致它圍繞著它的錨點旋轉(zhuǎn)。我們可以decelerationBahavior在添加的時候嘗試刪除springBehavior,但是這樣會反過來將速度調(diào)整為零。

經(jīng)過一番調(diào)試我注意到問題在于計算彈簧的錨點。動畫是離散的,所以例如當滾動視圖被推到左邊時,bounds.origin可以取下列值:

x               y
46.984947   78.164795
36.891747   82.781387
20.600927   90.232750
8.031227    95.982079
-4.141042   101.549622
-13.288508  105.733643

x一直不等于0,所以我們必須通過求解兩個線性方程組y來x = 0手動計算:

y_1 = a*x_1 + b
y_2 = a*x_2 + b

這樣,我們就可以計算出bounds.origin左邊的交叉點,并將彈簧連接到正確的位置。計算是類似的其他邊緣。這是一個最終的版本:


最終效果.gif

結論

我們學習了如何在不太常見的情況下使用Dynamics,以及如何利用基于協(xié)議的設計提供的靈活性。最終的結果非常接近UIKit提供的結果。不過,我不得不承認,代碼并不像Pop那樣明顯,因為:

  1. 我們不得不使用一個中介對象來動畫bounds。目前,我懷疑它是否會 - 無法在自定義屬性與Dynamics之間建立關系。
  2. UIAttachmentBehavior與Pop的不同POPSpringAnimation,它沒有velocity屬性,所以我們必須decelerationBehavior手動保留和計算錨點。

最終版本在GitHub上可用。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容