【问题标题】:Fixed labels in the selection bar of a UIPickerView修复了 UIPickerView 选择栏中的标签
【发布时间】:2010-09-26 22:01:50
【问题描述】:

在时钟应用程序中,计时器屏幕显示一个选择器(在UIDatePickerModeCountDownTimer 模式下可能是一个UIPicker),在选择栏中带有一些文本(在本例中为“小时”和“分钟”)。

(edit) Note that these labels are fixed: They don't move when the picker wheel is rolling.

有没有办法在标准UIPickerView 组件的选择栏中显示此类固定标签?

我没有找到任何可以帮助解决此问题的 API。一个建议是添加 UILabel 作为选择器的子视图,但这不起作用。


回答

我听从了 Ed Marty 的建议(在下方给出答案),并且成功了!不完美,但它应该愚弄人们。作为参考,这是我的实现,请随时改进...

- (void)viewDidLoad {
    // Add pickerView
    self.pickerView = [[UIPickerView alloc] initWithFrame:CGRectZero];
    [pickerView release];
    CGSize pickerSize = [pickerView sizeThatFits:CGSizeZero];
    CGRect screenRect = [[UIScreen mainScreen] applicationFrame];
    #define toolbarHeight           40.0
    CGFloat pickerTop = screenRect.size.height - toolbarHeight - pickerSize.height;
    CGRect pickerRect = CGRectMake(0.0, pickerTop, pickerSize.width, pickerSize.height);
    pickerView.frame = pickerRect;

    // Add label on top of pickerView
    CGFloat top = pickerTop + 2;
    CGFloat height = pickerSize.height - 2;
    [self addPickerLabel:@"x" rightX:123.0 top:top height:height];
    [self addPickerLabel:@"y" rightX:183.0 top:top height:height];
    //...
}

- (void)addPickerLabel:(NSString *)labelString rightX:(CGFloat)rightX top:(CGFloat)top height:(CGFloat)height {
#define PICKER_LABEL_FONT_SIZE 18
#define PICKER_LABEL_ALPHA 0.7
    UIFont *font = [UIFont boldSystemFontOfSize:PICKER_LABEL_FONT_SIZE];
    CGFloat x = rightX - [labelString sizeWithFont:font].width;

    // White label 1 pixel below, to simulate embossing.
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(x, top + 1, rightX, height)];
    label.text = labelString;
    label.font = font;
    label.textColor = [UIColor whiteColor];
    label.backgroundColor = [UIColor clearColor];
    label.opaque = NO;
    label.alpha = PICKER_LABEL_ALPHA;
    [self.view addSubview:label];
    [label release];

    // Actual label.
    label = [[UILabel alloc] initWithFrame:CGRectMake(x, top, rightX, height)];
    label.text = labelString;
    label.font = font;
    label.backgroundColor = [UIColor clearColor];
    label.opaque = NO;
    label.alpha = PICKER_LABEL_ALPHA;
    [self.view addSubview:label];
    [label release];
}

【问题讨论】:

  • 这适用于多个组件吗?我有 3 个组件,我正在尝试为每个组件添加一个标签。我观察到的是,所有 3 个标签都作为子视图添加到第一个组件。如何找到插入标签子视图的视图,以便标签适当可见?
  • 简而言之,诀窍是使用嵌入了 PickerView 和标签的 UIView。在 iOS 5 中无法直接向 PickerView 添加标签。
  • 最终结果 plox 的屏幕截图? :)

标签: iphone cocoa-touch uipickerview


【解决方案1】:

创建您的选择器,创建一个带有阴影的标签,然后将其推送到 selectionIndicator 视图下方的选择器子视图。

看起来像这样


UILabel *label = [[[UILabel alloc] initWithFrame:CGRectMake(135, 93, 80, 30)] autorelease];
label.text = @"Label";
label.font = [UIFont boldSystemFontOfSize:20];
label.backgroundColor = [UIColor clearColor];
label.shadowColor = [UIColor whiteColor];
label.shadowOffset = CGSizeMake (0,1);
[picker insertSubview:label aboveSubview:[picker.subviews objectAtIndex:5]]; 
//When you have multiple components (sections)...
//you will need to find which subview you need to actually get under
//so experiment with that 'objectAtIndex:5'
//
//you can do something like the following to find the view to get on top of
// define @class UIPickerTable;
// NSMutableArray *tables = [[NSMutableArray alloc] init];
// for (id i in picker.subviews) if([i isKindOfClass:[UIPickerTable class]]) [tables addObject:i];
// etc...

-- 提前付款

【讨论】:

  • 这适用于多个组件吗?我有 3 个组件,我正在尝试为每个组件添加一个标签。我观察到的是,所有 3 个标签都作为子视图添加到第一个组件。如何找到插入标签子视图的视图,以便标签适当可见?
  • 的出色答案已变成 UIPickerView here 的简洁子类。
  • 对于任何想知道为什么这种方法在 iOS 5+ 中不起作用的人:您仍然可以向 PickerView 添加标签,但它会保留在后台。方法“insertSubview: aboveSubview:”是存在的,但是无论选择栏是否被激活或者你有多少组件,子视图总是空的。解决方案是(如@squelart 的问题所示)使用视图,将 PickerView 嵌入为子视图并向视图添加标签,定位良好。
  • 错误答案.. 永远不要使用像“objectAtIndex:5”这样的幻数,这会假设 Apple 永远不会改变 UIPicker 的结构。
【解决方案2】:

几年前,我已经将 dizy 的答案变成了 UIPickerView 的一个类别。刚刚验证它仍然适用于 iOS SDK 4.3,并在此处发布。它允许您添加一个标签(XX 小时)并对这个标签进行动画更改(例如 1 小时 -> 3 小时),就像 UIDatePicker 所做的那样。

// UIPickerView_SelectionBarLabelSupport.h
//
// This file adds a new API to UIPickerView that allows to easily recreate
// the look and feel of UIDatePicker labeled components.
//
// Copyright (c) 2009, Andrey Tarantsov <andreyvit@gmail.com>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.


#import <Foundation/Foundation.h>


// useful constants for your font size-related code
#define kPickerViewDefaultTitleFontSize 20.0f
#define kDatePickerTitleFontSize 25.0f
#define kDatePickerLabelFontSize 21.0f


@interface UIPickerView (SelectionBarLabelSupport)

// The primary API to add a label to the given component.
// If you want to match the look of UIDatePicker, use 21pt as pointSize and 25pt as the font size of your content views (titlePointSize).
// (Note that UIPickerView defaults to 20pt items, so you need to use custom views. See a helper method below.)
// Repeated calls will change the label with an animation effect similar to UIDatePicker's one.
//
// To call this method on viewDidLoad, please call [pickerView layoutSubviews] first so that all subviews
// get created.
- (void)addLabel:(NSString *)label ofSize:(CGFloat)pointSize toComponent:(NSInteger)component leftAlignedAt:(CGFloat)offset baselineAlignedWithFontOfSize:(CGFloat)titlePointSize;

// A helper method for your delegate's "pickerView:viewForRow:forComponent:reusingView:".
// Creates a propertly positioned right-aligned label of the given size, and also handles reuse.
// The actual UILabel is a child of the returned view, use [returnedView viewWithTag:1] to retrieve the label.
- (UIView *)viewForShadedLabelWithText:(NSString *)label ofSize:(CGFloat)pointSize forComponent:(NSInteger)component rightAlignedAt:(CGFloat)offset reusingView:(UIView *)view;

// Creates a shaded label of the given size, looking similar to the labels used by UIPickerView/UIDatePicker.
- (UILabel *)shadedLabelWithText:(NSString *)label ofSize:(CGFloat)pointSize;

@end

以及实现:

// UIPickerView_SelectionBarLabelSupport.m
//
// This file adds a new API to UIPickerView that allows to easily recreate
// the look and feel of UIDatePicker labeled components.
//
// Copyright (c) 2009, Andrey Tarantsov <andreyvit@gmail.com>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

#import "UIPickerView_SelectionBarLabelSupport.h"


// used to find existing component labels among UIPicker's children
#define kMagicTag 89464534
// a private UIKit implementation detail, but we do degrade gracefully in case it stops working
#define kSelectionBarClassName @"_UIPickerViewSelectionBar"

// used to sort per-component selection bars in a left-to-right order
static NSInteger compareViews(UIView *a, UIView *b, void *context) {
    CGFloat ax = a.frame.origin.x, bx = b.frame.origin.x;
    if (ax < bx)
        return -1;
    else if (ax > bx)
        return 1;
    else
        return 0;
}


@implementation UIPickerView (SelectionBarLabelSupport)

- (UILabel *)shadedLabelWithText:(NSString *)label ofSize:(CGFloat)pointSize {
    UIFont *font = [UIFont boldSystemFontOfSize:pointSize];
    CGSize size = [label sizeWithFont:font];
    UILabel *labelView = [[[UILabel alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)] autorelease];
    labelView.font = font;
    labelView.adjustsFontSizeToFitWidth = NO;
    labelView.shadowOffset = CGSizeMake(1, 1);
    labelView.textColor = [UIColor blackColor];
    labelView.shadowColor = [UIColor whiteColor];
    labelView.opaque = NO;
    labelView.backgroundColor = [UIColor clearColor];
    labelView.text = label;
    labelView.userInteractionEnabled = NO;
    return labelView;
}

- (UIView *)viewForShadedLabelWithText:(NSString *)title ofSize:(CGFloat)pointSize forComponent:(NSInteger)component rightAlignedAt:(CGFloat)offset reusingView:(UIView *)view {
    UILabel *label;
    UIView *wrapper;
    if (view != nil) {
        wrapper = view;
        label = (UILabel *)[wrapper viewWithTag:1];
    } else {
        CGFloat width = [self.delegate pickerView:self widthForComponent:component];

        label = [self shadedLabelWithText:title ofSize:pointSize];
        CGSize size = label.frame.size;
        label.frame = CGRectMake(0, 0, offset, size.height);
        label.tag = 1;
        label.textAlignment = UITextAlignmentRight;
        label.autoresizingMask = UIViewAutoresizingFlexibleHeight;

        wrapper = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, width, size.height)] autorelease];
        wrapper.autoresizesSubviews = NO;
        wrapper.userInteractionEnabled = NO;
        [wrapper addSubview:label];
    }
    label.text = title;
    return wrapper;
}

- (void)addLabel:(NSString *)label ofSize:(CGFloat)pointSize toComponent:(NSInteger)component leftAlignedAt:(CGFloat)offset baselineAlignedWithFontOfSize:(CGFloat)titlePointSize {
    NSParameterAssert(component < [self numberOfComponents]);

    NSInteger tag = kMagicTag + component;
    UILabel *oldLabel = (UILabel *) [self viewWithTag:tag];
    if (oldLabel != nil && [oldLabel.text isEqualToString:label])
        return;

    NSInteger n = [self numberOfComponents];
    CGFloat total = 0.0;
    for (int c = 0; c < component; c++)
        offset += [self.delegate pickerView:self widthForComponent:c];
    for (int c = 0; c < n; c++)
        total += [self.delegate pickerView:self widthForComponent:c];
    offset += (self.bounds.size.width - total) / 2;

    offset += 2 * component; // internal UIPicker metrics, measured on a screenshot
    offset += 4; // add a gap

    CGFloat baselineHeight = [@"X" sizeWithFont:[UIFont boldSystemFontOfSize:titlePointSize]].height;
    CGFloat labelHeight = [@"X" sizeWithFont:[UIFont boldSystemFontOfSize:pointSize]].height;

    UILabel *labelView = [self shadedLabelWithText:label ofSize:pointSize];
    labelView.frame = CGRectMake(offset,
                                 (self.bounds.size.height - baselineHeight) / 2 + (baselineHeight - labelHeight) - 1,
                                 labelView.frame.size.width,
                                 labelView.frame.size.height);
    labelView.tag = tag;

    UIView *selectionBarView = nil;
    NSMutableArray *selectionBars = [NSMutableArray array];
    for (UIView *subview in self.subviews) {
        if ([[[subview class] description] isEqualToString:kSelectionBarClassName])
            [selectionBars addObject:subview];
    }
    if ([selectionBars count] == n) {
        [selectionBars sortUsingFunction:compareViews context:NULL];
        selectionBarView = [selectionBars objectAtIndex:component];
    }
    if (oldLabel != nil) {
        [UIView beginAnimations:nil context:oldLabel];
        [UIView setAnimationDuration:0.25];
        [UIView setAnimationDelegate:self];
        [UIView setAnimationDidStopSelector:@selector(YS_barLabelHideAnimationDidStop:finished:context:)];
        oldLabel.alpha = 0.0f;
        [UIView commitAnimations];
    }
    // if the selection bar hack stops working, degrade to using 60% alpha
    CGFloat normalAlpha = (selectionBarView == nil ? 0.6f : 1.0f);
    if (selectionBarView != nil)
        [self insertSubview:labelView aboveSubview:selectionBarView];
    else
        [self addSubview:labelView];
    if (oldLabel != nil) {
        labelView.alpha = 0.0f;
        [UIView beginAnimations:nil context:oldLabel];
        [UIView setAnimationDuration:0.25];
        [UIView setAnimationDelay:0.25];
        labelView.alpha = normalAlpha;
        [UIView commitAnimations];
    } else {
        labelView.alpha = normalAlpha;
    }
}

- (void)YS_barLabelHideAnimationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(UIView *)oldLabel {
    [oldLabel removeFromSuperview];
}

@end

使用示例(在视图控制器中):

- (void)updateFloorLabel {
    NSInteger floor = [self.pickerView numberOfRowsInComponent:0] - [self.pickerView selectedRowInComponent:0];
    NSString *suffix = @"th";
    if (((floor % 100) / 10) != 1) {
        switch (floor % 10) {
            case 1:  suffix = @"st"; break;
            case 2:  suffix = @"nd"; break;
            case 3:  suffix = @"rd"; break;
        }
    }
    [self.pickerView addLabel:[NSString stringWithFormat:@"%@ Floor", suffix]
                       ofSize:21
                  toComponent:0
                leftAlignedAt:50
baselineAlignedWithFontOfSize:25];    
}

- (void)viewDidLoad {
  ...
  [self.pickerView layoutSubviews];
  [self updateFloorLabel];
  ...
}

- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
    NSString *s = [NSString stringWithFormat:@"%d", [pickerView numberOfRowsInComponent:0] - row];
    return [pickerView viewForShadedLabelWithText:s ofSize:25 forComponent:0 rightAlignedAt:46 reusingView:view];
}

- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
    [self updateFloorLabel];
}

享受吧!

【讨论】:

  • 我试过你的解决方案,但得到 [SettingsViewController pickerView:widthForComponent:]: unrecognized selector sent to instance 有什么问题?在线 CGFloat width = [self.delegate pickerView:self widthForComponent:component];
  • @Andrey,我已经在这里使用了你很棒的小代码一段时间了。在 iOS 7/8 下,看起来您的 selectionBarView hack 确实已经消失了,但是由于您使用 0.6 alpha 的优雅恢复,代码仍然可以运行。黑客攻击的原因是什么?换句话说,alpha 回退丢失了什么?此外,我在附近的某个地方偶尔会出现动画崩溃。我不认为是这个代码,因为oldLabel还没有设置,所以没有动画,但是你认为这里可能有什么问题吗?
【解决方案3】:

假设我们要实现一个选择距离的选择器视图,有2列,一列距离,一列单位,即km。然后我们希望修复第二列。我们可以通过一些委托方法来实现。

- (NSString*)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
{
    if (component == 0) {
        return self.distanceItems[row];
    }
    else {
        return @"km";
    }
}

-(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView{
    return 2;
}

-(NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component{
    if (component == 0) {
        return [self.distanceItems count];
    }
    else {
    // when it comes to the second column, only one row.
        return 1;
    }
}

现在我们有了这个:

我想这是最简单的方法。

【讨论】:

  • 最初这个解决方案看起来不错,但是:(1)数字和单位之间的距离太大(任何想法如何使它更小?),(2)单位“km”对触摸有反应并向上/向下移动,而 iOS 计时器中的单位是固定的(任何想法如何让它静止?)
【解决方案4】:

你可以做两件事:

如果每一行和每一行中的组件都是一个简单的文本,那么您可以简单地使用默认的 UIPickerView 实现,并在您的控制器中实现以下UIPickerViewDelegate 方法:

  • - (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component 跟踪选择了哪一行

  • 并在- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component的实现中为所选行返回不同的文本

如果您需要将文本以外的其他内容作为所选行的区分符,那么您基本上需要创建自己的 CustomPickerView 派生自 UIPickerView,然后

  • 首先实现- (void)selectRow:(NSInteger)row inComponent:(NSInteger)component animated:(BOOL)animated 并跟踪选择了哪一行。

  • 然后实现- (UIView *)viewForRow:(NSInteger)row forComponent:(NSInteger)component为选中的行生成不同的视图。

SDK 中提供了使用 UIPickerView 或实现自定义 UIPickerView 的示例,称为 UICatalog。

【讨论】:

  • 不错的技巧,除了一件事:使用您的方法,标签不像我要求的那样在计时器选择器中固定为“小时”。我想它仍然有点帮助,至少有一个关于每个选择器组件指的是什么的提示。但是一旦你真正让它滚动,幻觉就会消失......
  • 另外,UICatalog 示例在标准或自定义 UIPickerView 中没有这些固定标签...这可能意味着它可能不是由框架提供的。这似乎很愚蠢,因为他们已经为 Timer 选择器实现了它!
【解决方案5】:

我收到了一个在 iOS 7 中运行良好的question 答案,这是一个非常酷的trick

这个想法是创建多个组件,并且对于那些标签组件,指定它是单行。对于某些人的浮雕外观,您可以使用委托方法返回 NSAttributedStrings:

- (NSAttributedString *)pickerView:(UIPickerView *)pickerView attributedTitleForRow:(NSInteger)row forComponent:(NSInteger)component

【讨论】:

    【解决方案6】:

    与其在 UIPickerView 中添加标签,不如将其粘贴在其顶部,作为与其重叠的兄弟。唯一有问题的是如何获得相同的字体。我不知道如何获得那种浮雕外观,但也许其他人会这样做,在这种情况下,这根本不是问题。

    【讨论】:

      【解决方案7】:

      要重新创建标签的浮雕外观...只需使用文本创建图像,这样您就可以轻松地将非常相似的效果应用于文本...然后使用 UIImageViews 而不是标签

      【讨论】:

        【解决方案8】:

        我也遇到了同样的问题。您可以在我在 GitHub 上发布的自定义时间选择器中看到工作示例:
        https://github.com/kgadzinowski/iOSSecondsTimerPicker
        它完全符合您的要求。

        【讨论】:

        【解决方案9】:

        你能说明你定义pickerTop和pickerSize的地方吗?

            CGFloat pickerTop = timePicker.bounds.origin.y;
        CGSize pickerSize = timePicker.bounds.size;
        

        这就是我所拥有的,但 pickerTop 似乎是错误的。

        麦克

        【讨论】:

        • 代码无法放入评论,所以我把它放在上面的答案中。请注意一点魔力(40),这可能需要根据您是否使用导航栏或其他占用应用程序上方屏幕空间的东西来改变......如果有人知道更好的方法,请告诉我!跨度>
        【解决方案10】:
        【解决方案11】:

        请注意,xib 编辑器也允许添加子视图,因此您可以避免使用过多的编码和对尺寸的猜测。

        【讨论】:

        • 你能提供一些代码吗?
        【解决方案12】:

        水平对齐多个选择器,它们之间没有空格,并关闭静态列的用户交互。

        在每个picker上设置一个标签,以确定哪个是数据源调用中的pickerView。静态列的行数显然是 1。

        这也可以更好地控制宽度和位置。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-10-05
          • 1970-01-01
          相关资源
          最近更新 更多