プロパティ変更アニメーションでビューを作成する

iOS用のアプリケーションを開発する際の典型的なタスクの1つは、カスタムUI要素を作成することです。これには、プロパティの値の変化をアニメーション化することが必要になる場合があります。 この記事では、アニメーションで値を変更できるプロパティを持つUIViewのサブクラスを作成するプロセスについて説明します。 簡単な例:0〜1の範囲の色と値の変化をアニメーション化する機能を備えた円形の進行を描く必要があります。







カスタムアニメーションを作成するには、インターフェイスでQuartz CoreおよびCore Animationツールを使用します。 主な作業はレイヤークラスで行われますが、実際には、ユーザーインターフェイスは通常ビュー階層から構築されるため、 UIViewの個別のサブクラスの作成が考慮されます。 同じ理由で、ARCを使用します。 始めましょう。



フレームワーク



まず、プロジェクトにQuartz Coreフレームワークがない場合は接続する必要があります。



レイヤー



次に、レイヤークラスを作成する必要があります。 TSTRoundProgressLayer呼びCALayerから継承します 。 外の世界と対話するには、彼はインターフェイスが必要になります。 UIProgressViewのような標準コントロールの方法で作成しましょう。



@interface TSTRoundProgressLayer : CALayer @property (strong, nonatomic) __attribute__((NSObject)) CGColorRef progressColor; - (void)setProgressColor:(CGColorRef)progressColor animated:(BOOL)animated; @property (readwrite, nonatomic) CGFloat progress; - (void)setProgress:(CGFloat)progress animated:(BOOL)animated; @end
      
      





プロパティ内のカラーストレージに注意する価値があります。 Core Animationは、色の変化をアニメーション化できますが、 CGColorRefの場合のみです。 ARCは当初、CGワールドのオブジェクトを保存する方法を理解していないため、追加のメモリ管理属性を設定する必要があります。



進歩の可能な意味は制限する意味があります。 これを行うには、このプロパティをクラス拡張で複製する必要があります(理由については後で詳しく説明します)。 クラスの外部からアクセス可能なプロパティは、値を変更するロジックを追加するためにのみ必要であり、アニメーションに直接関連するすべての作業は、拡張機能からのペアを通じて行われます。 たとえば、 animatableProgressと呼びましょう。



 @interface TSTRoundProgressLayer () @property (assign, nonatomic) CGFloat animatableProgress; @end
      
      





アニメーションを機能させるには、いくつかの手順を実行する必要があります。



ステータスレンダリングコード



 - (void)drawInContext:(CGContextRef)context { CGFloat lineWidth = [UIScreen mainScreen].scale; CGRect rect = self.bounds; if (rect.size.height <= lineWidth || rect.size.width <= lineWidth) return; rect = CGRectInset(rect, lineWidth, lineWidth); CGFloat radius = MIN(rect.size.height, rect.size.width)/2; CGContextSetLineWidth(context, lineWidth); CGContextSetStrokeColorWithColor(context, self.progressColor); CGContextBeginPath(context); CGContextAddArc(context, CGRectGetMidX(rect), CGRectGetMidY(rect), radius, -M_PI_2, -M_PI_2 + M_PI*2*self.animatableProgress, NO); CGContextStrokePath(context); }
      
      





まず、誤った条件を制限します。 次に、描画領域を少し狭めて、レイヤーの端で線が切れないようにします(Core Graphicsで線を描画するコスト)。 次に、計算を実行し、コンテキストを調整し、直接描画します。 コードはanimatableProgress内部プロパティの値を使用することに注意してください。 また、 進行状況が割り当てられていない場合ゼロになり、コードが正常に機能することにも注意してください。 一般に、黒がデフォルトの進行色に合っている場合、 progressColorも空になります。 ただし、別のデフォルト色を設定する場合は、 + defaultValueForKeyメソッドを使用できます。



 + (id)defaultValueForKey:(NSString *)key { if ([key isEqualToString:NSStringFromSelector(@selector(progressColor))]) { return (id)[UIColor blueColor].CGColor; } return [super defaultValueForKey:key]; }
      
      





フィールド名に関しては、次の組み合わせの保険を常にお勧めします。



 NSStringFromSelector(@selector())
      
      





そのため、フィールドの名前を変更すると、コンパイラは修正が必要な場合に通知します。

重要な点は、描画コードがレイヤーの表面の小さな円だけを塗りつぶし、割り当てられたスペース全体を塗りつぶさないことです。したがって、正しいレンダリングを行うには、 opaqueプロパティをNOに設定するか、 backgroundColorが1以外のアルファを持つ必要があります。 isOpaueゲッターをオーバーロードできます:



 - (BOOL)isOpaque { return NO; }
      
      





さらに、この例の描画コード自体は、 clearsContextBeforeDrawing == YESビューが必要になるように記述されています(これがデフォルト値です)。



プロパティを変更するときに再描画するかどうかを指定します



プロパティ値が変更されたときにレイヤーを再描画する必要があることをレイヤーが認識するためには、 + needsDisplayForKeyメソッドをオーバーロードする必要があります



 + (BOOL)needsDisplayForKey:(NSString *)key { if ([key isEqualToString:NSStringFromSelector(@selector(progressColor))] || [key isEqualToString:NSStringFromSelector(@selector(animatableProgress))]) { return YES; } return [super needsDisplayForKey:key]; }
      
      





動的プロパティ



また、プロパティ値を変更するときにCALayerの魔法をトリガーするには、それらを動的にする必要があります。



 @dynamic progressColor, progress;
      
      





どのように機能しますか? アニメーションを管理するために、 CALayerには、ivarとアクセサの実装を持たないキーによる値の処理を提供する機能があります。 まず、アクセサは動的プロパティ用に合成されません。 したがって、アクセサが動的プロパティ(たとえば、 -setAnimatableProgress :)から呼び出されるとセレクタは認識されず、状況を解決するためにランタイムメカニズムがアクティブになります。 クラス+(BOOL)resolveInstanceMethod:(SEL)selのメソッドがトリガーされ、メソッドがこのクラスの既存の動的プロパティと一致する場合、アニメーションメカニズムを開始する実装に追加されます。



アニメーションを作成する



最後に、レイヤーに追加されるアニメーションオブジェクト自体を作成する必要があります。 これを行うには、 -actionForKeyメソッドを使用します。



 - (id<CAAction>)actionForKey:(NSString *)key { if ([key isEqualToString:NSStringFromSelector(@selector(progressColor))] || [key isEqualToString:NSStringFromSelector(@selector(animatableProgress))]) { CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:key]; animation.duration = 1; animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; animation.fromValue = [self.presentationLayer valueForKey:key]; return animation; } return [super actionForKey:key]; }
      
      





ここで、目的のキーについて、対応するアニメーションを返す必要があります。 一般に、 CAActionプロトコルに準拠するオブジェクトを返す必要があります。これにより、 「オブジェクトはCALayerによって起動されたアクションに応答する」ことができます(無料翻訳)。 iOS SDKクラスのうち、 CAAnimationのみが実装しています。 プロトコルの説明はかなり曖昧であり、議論から判断すると、自分でプロトコルを実装することはほとんど意味がありません。 さらに、 CAAnimationには十分な柔軟性があり、通常のアプリケーションのインターフェースのアニメーションに関連する問題の大部分を解決できます。



選択した例では、最もシンプルで最高レベルのオプション-CABasicAnimation-がすべて実行されます。 ここで、アニメーション化するキーの値、実行する時間、開始点を指定するだけで十分です。 必要に応じて、より柔軟なタイプのアニメーションを使用して微調整できます。 そのため、例えば、対応する一時関数を示すイジングを追加しました。



プロパティの値が変更される前にアニメーションが作成され、その後動作を開始するため、最終的な値の表示は省略されていることに注意してください-アニメーションの作成時には、単にそれを取得する場所はありません。 同様に、 CABasicAnimationは開始時のkeyPath値を初期および最終デフォルトとして使用するため、この場合、すべてが正常に機能します。



-addAnimation:forKey:を呼び出すと、アニメーションがレイヤーに追加されます。 キーCABasicAnimationのkeyPathを混同しないでください。これらは直接関連していないためです。 たとえば、「foo」プロパティのアニメーションを追加するには、「bar」キーでレイヤーに追加できます。



 CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"foo"]; [layer addAnimation:animation forKey:@"bar"];
      
      





レイヤー自体が-actionForKey:を使用してアニメーションを追加すると、同じキーを使用して追加され、変更されます(このメソッドに渡されます)。 この場合、アニメーションオブジェクトを作成するためのコードは、 -actionForKey:キーがそのkeyPathと一致するように記述されています。 あるアニメーションを別のアニメーションが表示されている間に開始でき、キーによって新しいアニメーションが追加されると、同じキーによる現在のアニメーションが削除されることが重要です。 これは、あるアニメーションが別のアニメーションに置き換わる場合、ジョイントのスムージングについて考える必要があることを意味します。



画面にレイヤーを表示するために、システムは、プロパティをアニメーション化するオブジェクトではなく、「プレゼンテーションレイヤー」( presentationLayer )-システムによって作成されたコピーを使用します。 それから、アニメーション化されたプロパティの実際の値を取得できます。 したがって、アニメーションの初期値を取得するために使用されます。作成時に画面に表示された値から新しいアニメーションを開始します。



これにより、現在のプロパティのアニメーションオブジェクト自体が作成されます。 値を変更するときのレイヤーの動作は、 backgroundColorなどのプロパティを変更するときと同じになることに注意してください-デフォルトでは、変更はアニメーション化されます。 ビューレベルでは、これを変更できます。



外部インターフェース



基礎があり、外部インターフェイスの作業を整理する必要があります。



 - (void)setProgressColor:(CGColorRef)progressColor animated:(BOOL)animated { self.progressColor = progressColor; if (!animated) { [self removeAnimationForKey:NSStringFromSelector(@selector(progressColor))]; } } - (CGFloat)progress { return self.animatableProgress; } - (void)setProgress:(CGFloat)progress { self.animatableProgress = MAX(0, MIN(progress, 1)); } - (void)setProgress:(CGFloat)progress animated:(BOOL)animated { self.progress = progress; if (!animated) { [self removeAnimationForKey:NSStringFromSelector(@selector(animatableProgress))]; } }
      
      





既に述べたように、レイヤーは変化するキーにアニメーションを追加します。 したがって、変更を即座に表示するには、値を変更した後、キーによるアニメーションを削除するだけで十分です。



ここでは、値の範囲を制限するために外部プログレスプロパティのアクセサーがどのように使用されるかを確認します。 拡張機能に非表示のペアを追加する必要があるのはなぜですか? progressプロパティのみがあり、 -setProgress:をオーバーロードする場合、 CALayerはアニメーショントリガーするためにランタイムにメソッド実装を追加しません。 -setValue:forKey:をオーバーロードし、値を変更したチェックを追加するという単純なアイデアがありましたが、値を変更するとこのメソッドがバイパスされますが、アニメーション表示中にpresentationLayerで呼び出されます。 アニメーションオブジェクトの作成時に値を指定して値を制限するというアイデアがありましたが、この時点では最終的な値はまだわかりません。 したがって、外部プロパティを複製し、その非表示ペアを使用してアニメーションを操作し、外部アクセッサを使用してロジックを追加するだけです。



表示する



レイヤーの操作は完了しました。今度は、ビューでラップする必要があります。 これを行うには、同様のインターフェイスを持つ新しいクラスを追加します。



 @interface TSTRoundProgressBar : UIView @property (readwrite, nonatomic) UIColor *progressColor; - (void)setProgressColor:(UIColor*)progressColor animated:(BOOL)animated; @property (readwrite, nonatomic) CGFloat progress; - (void)setProgress:(CGFloat)progress animated:(BOOL)animated; @end
      
      





Quartz Coreを接続し、ビュークラスをレイヤークラスに設定します。



 + (Class)layerClass { return [TSTRoundProgressLayer class]; }
      
      





すべてのインターフェイスは、呼び出しをレイヤーに転送するためだけに必要です。



 - (UIColor *)progressColor { return [UIColor colorWithCGColor:[(TSTRoundProgressLayer*)self.layer progressColor]]; } - (void)setProgressColor:(UIColor *)progressColor { [(TSTRoundProgressLayer*)self.layer setProgressColor:progressColor.CGColor animated:NO]; } - (void)setProgressColor:(UIColor *)progressColor animated:(BOOL)animated { [(TSTRoundProgressLayer*)self.layer setProgressColor:progressColor.CGColor animated:animated]; } - (CGFloat)progress { return [(TSTRoundProgressLayer*)self.layer progress]; } - (void)setProgress:(CGFloat)progress { [(TSTRoundProgressLayer*)self.layer setProgress:progress animated:NO]; } - (void)setProgress:(CGFloat)progress animated:(BOOL)animated { [(TSTRoundProgressLayer*)self.layer setProgress:progress animated:animated]; }
      
      





ここで、ビヘイビアを表示により馴染みのあるビューにします-デフォルトでは、値の変更はアニメーション化されません。



使用する



実際、クラスはすぐに使用できます。 これで、ビューを階層に配置し、値が変化したときの動作を観察できます。 たとえば、進行状況が追加されるビュー階層にコントローラークラスを作成できます。 ビューの追加の詳細は説明しませんが、便利な方法で実行できます。 ストーリーボードを使用しました。 だから私たちは持っています:



 @interface ViewController () @property (weak, nonatomic) IBOutlet TSTRoundProgressBar *progressBar; @end
      
      





たとえば、次のように動作を観察できます。



 - (void)viewDidLoad { [super viewDidLoad]; self.progressBar.progress = 0.2; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.progressBar setProgressColor:[UIColor magentaColor] animated:YES]; [self.progressBar setProgress:0.9 animated:YES]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.8 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.progressBar setProgressColor:[UIColor greenColor] animated:YES]; [self.progressBar setProgress:0.4 animated:YES]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ self.progressBar.progressColor = [UIColor redColor]; self.progressBar.progress = 0.9; }); }); }); }
      
      





ビューのコンテンツは描画を使用して作成されるため、 contentMode == UIViewContentModeRedrawを設定すると、 フレームが変更されたときにコンテンツが再び描画されるので便利です。 これは、ビューの外部または初期化中の内部のコードで、またはインターフェイスビルダーで実行できます。 コードの純度のために、最後のオプションを選択しました。



完成したプロジェクトの例は、 ここにあります



All Articles