属性付きラベルを作る その3


前回に続き、属性付き文字列を表示する話。

属性付き文字列を表示したら、次はそれをタッチしたくなるのが人情というものだな。えーっと、想定しているのは、文中にリンクを埋め込みたい、ということね。文章の一部を青字、または下線付きで表示する事で、リンクがあることを表す。ユーザがそれをタップしたら、情報を表示したり、別のページへジャンプする。ってことをやりたいよね。ということで、タッチイベントを拾ってみよう。

タッチイベントを受け取ること自体は簡単だ。いま作っているラベルはUILabelのサブクラスなので、touchesEnded:withEvent:を上書きすればいい。問題は、タッチしたのが文字列のどの部分なのかを特定する事だ。これを実現するには、Core TextのAPIを使って、文字列の位置情報を特定していく必要がある。

いまのところ、CTFrameというオブジェクトでテキストの描画を行っている。ここから、まず始めに、行の情報を取り出そう。CTLineというオブジェクトが行を表す。CTFrameからは、CTLineの配列と、それぞれの行の原点座標を取り出す事ができる。

CTLineからは、その行が持つタイポグラフィの情報を取得できる。ascentとかdescentとか、そういうやつ。余談だけど、ascentをアセント、descentをディセント、ってカタカナで書くと、何の事だか分かんなくなるな。アルファベットの方が理解しやすいと思う。これで、CTLineの領域が分かる。したがって、どの行をタッチしたか分かる事になる。そしてさらに、CTLineに対して座標を指定すると、テキストの何文字目にあたるかが分かる。これで、タッチした文字まで特定できるという訳だ。

以上が理屈。では、これをもとに実装をしてみよう。

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
    // Get touch point
    UITouch*    touch;
    CGPoint     point;
    touch = [touches anyObject];
    point = [touch locationInView:self];
    point.y = CGRectGetHeight(self.bounds) - point.y;

    // Get lines
    CFArrayRef  lines;
    lines = CTFrameGetLines(_ctFrame);

    // Get line origins
    CGPoint*    origins;
    origins = malloc(sizeof(CGPoint) * CFArrayGetCount(lines));
    CTFrameGetLineOrigins(_ctFrame, CFRangeMake(0, CFArrayGetCount(lines)), origins);

    int i;
    for (i = 0; i < CFArrayGetCount(lines); i++) {
        // Get line
        CTLineRef   line;
        line = CFArrayGetValueAtIndex(lines, i);

        // Get origin
        CGPoint origin;
        origin = *(origins + i);

        // Get typographics bounds
        float   ascent;
        float   descent;
        float   leading;
        double  width;
        width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);

        // Decide line frame
        CGRect  lineFrame;
        lineFrame.origin.x = origin.x;
        lineFrame.origin.y = origin.y - descent;
        lineFrame.size.width = width;
        lineFrame.size.height = ascent + descent;

        // Check with point
        if (CGRectContainsPoint(lineFrame, point)) {
            // Get index for position
            CFIndex index;
            index = CTLineGetStringIndexForPosition(line, point);
            if (index == kCFNotFound) {
                continue;
            }

            // Get attributes at position
            NSDictionary*   attrs;
            NSRange         range;
            attrs = [_attrStr attributesAtIndex:index effectiveRange:&range];

            // Notify to delegate
            if ([_delegate respondsToSelector:
                    @selector(attributedLabelTouchedAtIndex:attributes:effectiveRange:)])
            {
                [_delegate attributedLabelTouchedAtIndex:index 
                        attributes:attrs effectiveRange:range];
            }
        }
    }

    // Release objects
    if (origins) {
        free(origins), origins = NULL;
    }

    // Invoke super
    [super touchesEnded:touches withEvent:event];
}

CTFrameからCTLineの配列を取り出すのは、CTFrameGetLinesという関数だ。さらに、行の原点座標を取り出すのは、CTFrameGetLineOriginsという関数。こちらはCGPointの配列領域をあらかじめ確保しておく必要がある。

CTLineのタイポグラフィ情報は、CTLineGetTypographicBoundsで取得する。これを使うと、ascent、descent、leading、そして行の長さを知る事ができる。これらの情報と原点座標を組み合わせて、行のframeを計算する。タッチした座標から文字のインデックスを取り出すのは、CTLineGetStringIndexForPosition。これで、どの文字がタッチされたかが分かった。

タッチされたという情報は、デリゲートに通知する事にした。タッチした文字のインデックスに加えて、属性の情報も送ってやる。その文字がどんな属性を持っているかと、どの範囲までそれが続いているか、という情報だ。

これでできた!試しに動かしてみると、こんな感じ。文字列をタッチすると、その文字と属性とを取り出せているのが分かる。取り出した情報を元に、リンクだったら関連する動作をさせればいい。

ここまでのソースコード:AttrLabel-3.zip

  1. コメントはまだありません。

  1. 2011年 12月11日
    トラックバック先 :iOS開発備忘録 | source lab. note