[iOS] 1から0の間に正規化すると便利。そんでそれを利用したスライダーとか

2012/09/21

こんにちは。きんくまです。

正規化(normalization)という言葉があります。

wikiによると

正規化(せいきか、normalization)とは、
データ等々を一定のルール(規則)に基づいて変形し、利用しやすくすること。

だそうです。
そんで、いろんな分野で利用されてるみたいなんですけど、自分もたまに利用してマス。気づかずに使ってる人もいると思います。

自分の場合は0から1の間に正規化してます。
クライアント系のプログラムだとTweenのイージング関数とか、テクスチャのUVとかもこの値が使われてたりしますよね。

正規化を利用したスライダーの例

normalize1

今回作成したのは、正規化を利用したスライダーです。
スライダーを動かすと、そのときの数値がラベルに表示されて、また緑の円の大きさがそれに合わせてかわります。

このときに、スライダーから出てくる数値が常に1から0であれば、

現在の円の大きさ = 円の最大の大きさ x スライダーの数値

で表すことができます。
また、このスライダーを利用して様々なものを作れます。
例えば、これをスクロールバーにしたりとか。

ここで大事なことは、スライダーのタブの位置情報が1から0に変換されているということです。スライダーがすごいとかじゃなくて。
タブのX座標という具体的な情報が、1から0という他のところで使いやすい値に抽象化されているところがポイントです。

具体的なコードです。
ARC使用 iOS5以上
QuartzCore.frameworkを足しておいてください。

KKSliderView.h

#import <UIKit/UIKit.h>

#ifndef RGB
#define RGB(r,g,b) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:1]
#endif

@class KKSliderView;

@protocol KKSliderViewDelegate <NSObject>
@optional
- (void)sliderValueChanged:(KKSliderView *)sliderView value:(CGFloat)value;
@end

@interface KKSliderView : UIView
@property (nonatomic, weak) id<KKSliderViewDelegate> delegate;
@end

KKSliderView.m

#import "KKSliderView.h"
#import <QuartzCore/QuartzCore.h>


const CGFloat kKKSliderTabWidth = 44.0;
const CGFloat kKKSliderTabPadding = 2.0;

@interface KKSliderPartsLayer : CALayer
{
    UIColor *_fillColor;
}
- (id)initWithFillColor:(UIColor *)aFillColor frame:(CGRect)aFrame;
@end

@implementation KKSliderPartsLayer
- (id)initWithFillColor:(UIColor *)aFillColor frame:(CGRect)aFrame
{
    self = [KKSliderPartsLayer layer];
    if(self){
        _fillColor = aFillColor;
        self.frame = aFrame;
        self.contentsScale = [UIScreen mainScreen].scale;
        [self setNeedsDisplay];
    }
    return self;
}

- (void)drawInContext:(CGContextRef)ctx
{
    CGContextSetFillColorWithColor(ctx, _fillColor.CGColor);
    CGPathRef path = CGPathCreateCopy([UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:4.0].CGPath);
    CGContextAddPath(ctx, path);
    CGContextFillPath(ctx);
}
@end

@interface KKSliderView()
{
    CALayer *_tabLayer;
    CALayer *_tabBackgroundLayer;
    BOOL _dragging;
    CGSize _dragOffset;
    CGFloat _minTabX;
    CGFloat _maxTabX;
}

@end

@implementation KKSliderView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        _tabLayer = [[KKSliderPartsLayer alloc] initWithFillColor:RGB(43, 170, 154) frame:CGRectMake(kKKSliderTabPadding, kKKSliderTabPadding, kKKSliderTabWidth - kKKSliderTabPadding * 2, self.bounds.size.height - kKKSliderTabPadding * 2)];
        _tabBackgroundLayer = [[KKSliderPartsLayer alloc] initWithFillColor:RGB(38,48,37) frame:self.bounds];
        [self.layer addSublayer:_tabBackgroundLayer];
        [self.layer addSublayer:_tabLayer];
        _dragging = NO;
        
        _minTabX = kKKSliderTabPadding;
        _maxTabX = self.bounds.size.width - kKKSliderTabWidth + kKKSliderTabPadding;
    }
    return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self];
    if([_tabLayer hitTest:location]){
        _dragging = YES;
        _dragOffset = CGSizeMake(_tabLayer.frame.origin.x - location.x, _tabLayer.frame.origin.y - location.y);
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self];
    if(_dragging){
        [CATransaction begin];
        [CATransaction setValue:(id)kCFBooleanTrue
                         forKey:kCATransactionDisableActions];
        CGFloat posX = location.x + _dragOffset.width;
        if(posX < _minTabX){
            posX = _minTabX;
        }else if(posX > _maxTabX){
            posX = _maxTabX;
        }
        _tabLayer.frame = CGRectMake(posX, _tabLayer.frame.origin.y, _tabLayer.frame.size.width, _tabLayer.frame.size.height);
        [CATransaction commit];
        
        if([_delegate respondsToSelector:@selector(sliderValueChanged:value:)]){
            [_delegate sliderValueChanged:self value:[self normalizedValue]];
        }
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    _dragging = NO;
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    _dragging = NO;
}

- (CGFloat)normalizedValue
{
    return (_tabLayer.frame.origin.x - _minTabX) / (_maxTabX - _minTabX);
}
@end

delegateを設定すると、値がかわったときに
– (void)sliderValueChanged:(KKSliderView *)sliderView value:(CGFloat)value;
で呼び出されます。

使い方です。メインのViewController

KKViewController.h

#import <UIKit/UIKit.h>
#import "KKSliderView.h"

@interface KKViewController : UIViewController<KKSliderViewDelegate>

@end

KKViewController.m

#import "KKViewController.h"
#import <QuartzCore/QuartzCore.h>

const CGFloat kCircleLayerMaxDiameter = 200;
const CGPoint kCircleCenterPt = {320 * 0.5, 200 + kCircleLayerMaxDiameter * 0.5};

@interface KKViewController ()
{
    KKSliderView *_sliderView;
    UILabel *_currentValueLabel;
    CALayer *_circleLayer;
    CGPoint _circleCenterPt;
}

@end

@implementation KKViewController

- (void)loadView
{
    UIView *myView = [[UIView alloc] init];
    myView.backgroundColor = [UIColor whiteColor];
    _sliderView = [[KKSliderView alloc] initWithFrame:CGRectMake(50, 50, 220, 30)];
    _sliderView.delegate = self;
    [myView addSubview:_sliderView];
    
    _currentValueLabel = [[UILabel alloc] initWithFrame:CGRectMake(50, 120, 220, 36)];
    [_currentValueLabel setFont:[UIFont fontWithName:@"Helvetica-Bold" size:24]];
    _currentValueLabel.textAlignment = NSTextAlignmentCenter;
    [myView addSubview:_currentValueLabel];
    
    _circleLayer = [CAShapeLayer layer];
    _circleLayer.delegate = self;
    _circleLayer.frame = CGRectMake(kCircleCenterPt.x, kCircleCenterPt.y, 0, 0);
    _circleLayer.contentsScale = [UIScreen mainScreen].scale;
    [myView.layer addSublayer:_circleLayer];
    self.view = myView;
}

- (void)sliderValueChanged:(KKSliderView *)sliderView value:(CGFloat)value
{
    NSNumberFormatter *numFormatter = [[NSNumberFormatter alloc] init];
    [numFormatter setPositiveFormat:@"#.###"];
    _currentValueLabel.text = [numFormatter stringFromNumber:@(value)];
    
    CGFloat currentCircleDiameter = kCircleLayerMaxDiameter * value;
    
    _circleLayer.frame = CGRectMake(kCircleCenterPt.x - currentCircleDiameter * 0.5, kCircleCenterPt.y - currentCircleDiameter * 0.5, currentCircleDiameter, currentCircleDiameter);
    
    [_circleLayer setNeedsDisplay];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
    if([layer isEqual:_circleLayer]){
        CGContextSetFillColorWithColor(ctx, RGB(43, 170, 154).CGColor);
        CGContextFillEllipseInRect(ctx, layer.bounds);
    }
}

- (void)viewDidLoad
{
    [super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

ページトップへ戻る