I have a NSToolbarItem that uses a view similar to the Xcode status view. It currently has no label, but I can't figure out a way to draw into the area where the item label would normally be drawn. I would like the view to extend into that area just as the Xcode status view does. I know the very bottom portion of pixels of NSToolbar is out of bounds, but I have seen other applications draw into the label area. Any ideas?
Edit: For clarification, this is the status view I'm referring to in Xcode:
I want the bounds of my view to extend past the label area of the toolbar just as the view in Xcode does.
The Xcode status view is not an NSToolbarItem is a custom NSView inserted in the NSToolbar.
If you log
NSLog(#" %#", [[self.window.contentView superview] subviews]);
You will get
NSToolbarView doesn't autoresize it's subviews so you have issues with centering it.
And [self.window.contentView superview] doesn't contain the toolbarview when full screen.
You can add the view you want in the center of the toolbar to the [self.window.contentView superview] when not in fullscreen and position it properly. It will autoresize and stay centered.
When switching to full screen remove it from [self.window.contentView superview] and add it to NSToolbarView in the center that way it stays in the toolbar and it also moves down with the toolbar when you reveal the status bar.
You can get the toolbar view by iterating thru the subviews or with a private method
[[self.window toolbar] performSelector:#selector(_toolbarView)];
Update:
I did a little more digging with the debugger and I found out that this is what Xcode does. At least while not in full screen.
thealch3m1st$ sudo lldb
(lldb) process attach -p 11478
Process 11478 stopped
Executable module set to "/Applications/Xcode.app/Contents/MacOS/Xcode".
Architecture set to: x86_64.
(lldb) po [NSApplication sharedApplication]
(id) $0 = 0x000000040013f5e0 <IDEApplication: 0x40013f5e0>
(lldb) po [$0 mainWindow]
(id) $1 = 0x0000000000000000 <nil>
(lldb) po [$0 windows]
(id) $2 = 0x0000000408278460 <__NSArrayM 0x408278460>(
<IDEWelcomeWindow: 0x40141c1e0>,
<IDEWorkspaceWindow: 0x401ef2780>,
<NSComboBoxWindow: 0x402019be0>,
<NSWindow: 0x4022adc60>,
<IDEOrganizerWindow: 0x402951b20>
)
(lldb) po [$0 windows]
(id) $3 = 0x0000000408820300 <__NSArrayM 0x408820300>(
<IDEWelcomeWindow: 0x40141c1e0>,
<IDEWorkspaceWindow: 0x401ef2780>,
<NSComboBoxWindow: 0x402019be0>,
<NSWindow: 0x4022adc60>,
<IDEOrganizerWindow: 0x402951b20>
)
(lldb) [$3 objectAtIndex:1]
error: '[$3' is not a valid command.
(lldb) po [$3 objectAtIndex:1]
(id) $4 = 0x0000000401ef2780 <IDEWorkspaceWindow: 0x401ef2780>
(lldb) po [$4 contentView]
(id) $5 = 0x0000000401ef0920 <NSView: 0x401ef0920>
(lldb) po [$5 superview]
(id) $6 = 0x0000000401ef2e20 <NSThemeFrame: 0x401ef2e20>
(lldb) po [$6 subviews]
(id) $7 = 0x0000000401ef3800 <__NSArrayM 0x401ef3800>(
<_NSThemeCloseWidget: 0x401ef3120>,
<_NSThemeWidget: 0x401ef3b80>,
<_NSThemeWidget: 0x401ef40e0>,
<NSView: 0x401ef0920>,
<IDEActivityView: 0x4020cd700>,
<_NSThemeFullScreenButton: 0x402017b20>,
(<NSToolbarView: 0x4020192e0>: Xcode.IDEKit.ToolbarDefinition.Workspace),
<DVTDualProxyWindowTitleView: 0x40225e0a0>,
<NSThemeDocumentButton: 0x402698020>
)
(lldb) po [$7 objectAtIndex:4]
(id) $8 = 0x00000004020cd700 <IDEActivityView: 0x4020cd700>
(lldb) [$8 setHidden:YES]
error: '[$8' is not a valid command.
(lldb) po [$8 setHidden:YES]
(id) $9 = 0x0000000000000000 <nil>
(lldb) continue
Process 11478 resuming
(lldb)
And the activity view is gone :)
While in full screen however. It doesn't add it to NSToolbarView it adds it to NSNextStepFrame which is NSToolbarView's superview. The toolbar is not contained in the window's contentview superview when in full screen. I think it has something to do with full screen behavior and spaces.
You have to subclass NSToolbarItem:
- (id)initWithItemIdentifier:(NSString *)itemIdentifier {
self = [super initWithItemIdentifier:itemIdentifier];
if (self) {
self.hideLabel = NO;
}
return self;
}
- (NSView *)view {
NSView *view = [super view];
if (self.hideLabel) {
CGRect frame = view.frame;
frame.size.height = 45.0f;
frame.origin.y = 8.0f;
view.frame = frame;
}
return view;
}
- (NSString *)label {
return self.hideLabel ? #"" : [super label];
}
Create a toolbar:
NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:#"Toolbar"];
toolbar.delegate = self;
self.window.toolbar = toolbar;
Use NSToolbarDelegate to fill your toolbar with items:
- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar {
return [NSArray arrayWithObjects:#"Button", #"LCD", NSToolbarFlexibleSpaceItemIdentifier, NSToolbarSpaceItemIdentifier, nil];
}
- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar {
return [NSArray arrayWithObjects:#"Button", NSToolbarFlexibleSpaceItemIdentifier, #"LCD", NSToolbarFlexibleSpaceItemIdentifier, NSToolbarSpaceItemIdentifier, nil];
}
- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag {
MyToolbarItem *item = [[MyToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
if ([itemIdentifier isEqualToString:#"LCD"]) {
item.view = self.lcdView;
item.hideLabel = YES;
} else if ([itemIdentifier isEqualToString:#"Button"]) {
item.label = NSLocalizedString(#"Button", nil);
item.image = [NSImage imageNamed:#"Button"];
item.hideLabel = NO;
}
return item;
}
The lcd view should be (in this case) 32 points high before you hand it over to the toolbar item. If it's bigger, the toolbar will be too high.
The Xcode status view is actually a separate window floating over the toolbar. (This is easily tested: press ⇧⌘4 and press space to take a screen shot of a window, and hover the mouse over it.)
This code installs a window floating on top of the toolbar.
-(void)applicationWillFinishLaunching:(NSNotification *)aNotification {
NSRect winframe = [self.window frame];
NSRect viewrect = NSMakeRect(0, 0, 400, 50);
NSRect winrect = viewrect;
winrect.origin.x = NSMidX(winframe) - NSMidX(winrect);
winrect.origin.y = NSHeight(winframe) - NSHeight(winrect) - 18;
NSWindow* win = [[[NSWindow alloc] initWithContentRect:winrect styleMask: NSBorderlessWindowMask backing: NSBackingStoreBuffered defer: NO] autorelease];
[win setBackgroundColor:[NSColor clearColor]];
[win setOpaque:NO];
[win setIgnoresMouseEvents:YES];
MyStatusView* v = [[[MyStatusView alloc] initWithFrame:viewrect] autorelease];
[win setContentView: v];
[self.window addChildWindow:win ordered:NSWindowAbove];
}
The iTunes-XCode-LCD that extends in the label area is not a NSToolbarItem. Since NSToolbar isn't a NSView, you cannot add a subview to a NSToolbar instance.
But you can add a custom view directly in the window frame, that can be accessed through the contentView.superview property path of the NSWindow instance!
I.e. make your own subclass of NSWindowController and put some code like this in the 'windowDidLoad' method:
- (void)windowDidLoad
{
[super windowDidLoad];
NSImage *image = [NSImage imageNamed:#"lcd"];
NSRect lcdFrameRect = NSMakeRect(self.window.frame.size.width / 2 - image.size.width/2, self.window.frame.size.height - image.size.height - 20,
image.size.width, image.size.height);
NSImageView *lcdView = [[NSImageView alloc] initWithFrame: lcdFrameRect];
[lcdView setImage: image];
lcdView.autoresizingMask = NSViewMinYMargin | NSViewMinXMargin | NSViewMaxXMargin;
NSView * contentView = self.window.contentView;
[contentView.superview addSubview: lcdView];
}
This code will not work in Lion's full-screen mode, since the frame window isn't drawn when in fullscreen. To fix this, the view can be moved in a floating window, child of the main one (just check the NSWindow addChildWindow:ordered: method).
Related
I am trying to find a way to programmatically resize NSSplitViewItems.
Say the NSWindow frame size is 500x500px.
masterViewItem has a width of 100px, while subViewItem has a width of 400px.
When FOO, I want masterViewItem to collapse to 0px (towards the left),
while subViewItem expands to fill the entire window (500px).
When BAR, I want masterViewItem to expand back to 100px, while subViewItem collapses back to the original 400px.
- (IBAction) onTapOnSomeButton:(NSButton *)sender {
NSWindow *window = [[NSApplication sharedApplication] mainWindow];
NSSplitViewController *splitViewController = (NSSplitViewController *)[window contentViewController];
NSSplitViewItem *masterViewItem =[splitViewController.splitViewItems firstObject];
NSSplitViewItem *subViewItem = [splitViewController.splitViewItems lastObject];
if (FOO) {
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
// Expand subViewItem
} completionHandler:^{
}];
} else if (BAR) {
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
// Expand masterViewItem while shrinking subViewItem
} completionHandler:^{
}];
}
}
I am able to animate each item so that it seems to collapse/expand,
i.e.
subViewItem.viewController.view.animator.frame = CGRectMake(0, 0, window.frame.size.width, subViewItem.viewController.view.frame.size.height);
but the divider will not move, making the two items just move individually, and does not look like the entire screen is collapsing/expanding.
How would I implement this? Thanks in advance.
UPDATE
So I have done a bit more research, and found this:
How to animate the NSSplitView while resizing?
So I made code like this:
- (IBAction) onTapOnSomeButton:(NSButton *)sender {
NSWindow *window = [[NSApplication sharedApplication] mainWindow];
NSSplitViewController *splitViewController = (NSSplitViewController *)[window contentViewController];
NSSplitViewItem *masterViewItem =[splitViewController.splitViewItems firstObject];
NSSplitViewItem *subViewItem = [splitViewController.splitViewItems lastObject];
if (FOO) {
NSMutableDictionary *collapseMainAnimationDict = [NSMutableDictionary dictionaryWithCapacity:2];
[collapseMainAnimationDict setObject: subView forKey:NSViewAnimationTargetKey];
NSRect newRightSubViewFrame = subView.frame;
newRightSubViewFrame.size.width = splitViewController.splitView.frame.size.width;
[collapseMainAnimationDict setObject:[NSValue valueWithRect:newRightSubViewFrame] forKey:NSViewAnimationEndFrameKey];
NSMutableDictionary *collapseInspectorAnimationDict = [NSMutableDictionary dictionaryWithCapacity:2];
[collapseInspectorAnimationDict setObject: masterView forKey:NSViewAnimationTargetKey];
NSRect newLeftSubViewFrame = masterView.frame;
newLeftSubViewFrame.size.width = 0.0f;
newLeftSubViewFrame.origin.x = splitViewController.splitView.frame.size.width;
[collapseInspectorAnimationDict setObject:[NSValue valueWithRect:newLeftSubViewFrame] forKey:NSViewAnimationEndFrameKey];
NSViewAnimation *collapseAnimation = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObjects: collapseMainAnimationDict, collapseInspectorAnimationDict, nil]];
[collapseAnimation setDuration:0.3f];
[collapseAnimation startAnimation];
[splitViewController.splitView adjustSubviews];
[splitViewController.splitView setNeedsDisplay: YES];
} else if (BAR) {
// Not yet implemented
}
}
But to no effect. Any suggestions?
You have to resize the panes using [NSSplitView setPosition:ofDividerAtIndex:].
I have no idea what NSSplitViewController is, but presumably you can get access to the NSSplitView via its view property, or some such.
Rather than looking at it as a resize, it sounds like you're simply trying to collapse/uncollapse the master view item. You can just use NSSplitViewItem's collapsed property with its animator proxy to trigger the collapse:
if (FOO) {
// Expand subViewItem (collapse the master view item)
masterViewItem.animator.collapsed = YES;
And uncollapse:
} else if (BAR) {
// Expand masterViewItem while shrinking subViewItem
masterViewItem.animator.collapsed = NO;
}
}
With 10.11, there's API to designate your master view item as a "sidebar", which has additional collapse/uncollapse behavior for that item (such as overlays).
the problem I've met today is with my subclass of UIPageControl. When I initialize it, the frame (specifically the origin) and image of dots stays default, which is the problem, since I want it to change right after initialization. However, when I move with scrollView (as in "touch and move") after initialization, they (the dots) somehow jump to the right position with correct images.
What could be the problem?
Code:
CustomPageControl.m
- (id) initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
activeImage = [UIImage imageNamed:#"doton.png"];
inactiveImage = [UIImage imageNamed:#"dotoff.png"];
return self;
}
- (void) updateDots
{
for (int i = 0; i < [self.subviews count]; i++)
{
UIImageView *dot = [self.subviews objectAtIndex:i];
if (i == self.currentPage) dot.image = activeImage;
else dot.image = inactiveImage;
[dot setFrame:CGRectMake(i * 13.5, 1.5, 17, 17)];
}
}
- (void)setCurrentPage:(NSInteger)currentPage
{
[super setCurrentPage:currentPage];
[self updateDots];
}
#end
ChoosingView.m - init part
scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 160, 300)];
[scrollView setBackgroundColor:[UIColor clearColor]];
[scrollView setDelaysContentTouches:NO];
[scrollView setCanCancelContentTouches:YES];
[scrollView setClipsToBounds:NO];
[scrollView setScrollEnabled:YES];
[scrollView setPagingEnabled:YES];
[scrollView setShowsHorizontalScrollIndicator:NO];
[scrollView setShowsVerticalScrollIndicator:NO];
pageControl = [[CustomPageControl alloc] initWithFrame:CGRectMake(200, 300, 80, 20)];
[pageControl setBackgroundColor:[UIColor clearColor]];
pageControl.numberOfPages = 6;
[pageControl setCurrentPage:0];
the last line is when I would expect the UIPageControl to refresh, however that does not happen.
Does this happen with the standard UIPageControl implementation?
Your problem states that your objects subViews (eg the UIImageViews) rects/size are not initialising to your desired size/position.
I implemented this code in my project with a nib rather than programmatically and I needed to call -(void)updateDots to set it as its initial condition was the standard dots..
I dont see how the UIScrollView has any bearing impact on this unless somehow its linked to your -(void)updateDots function (E.g. your setting the currentIndex of your custom page control). You state, "However, when I move with scrollView (as in "touch and move") after initialization, they (the dots) somehow jump to the right position with correct images."
Because they "jump to the right position with correct images" it means that your -(void)updateDots function must be getting called. I dont see any other explanation.
Also your iteration loop assumes that all the UIViews in your .subViews array are UIImageViews, although fairly safe to assume this, I would check to see if the UIView is a UIImageView with reflection.
I've got a custom window popping up when an NSStatusItem is clicked. The code is based on MAAtachedwindow. Everything is working great but I can't figure out a way to dismiss the window when the user clicks on something else like another status bar item, or another app.
Here's my code for creating the window:
statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:width] retain];
//setup custom status menu view
CGFloat height = [[NSStatusBar systemStatusBar] thickness];
NSRect viewFrame = NSMakeRect(0.0f, 0.0f, width, height);
statusMenuView = [[[_ISStatusMenuView alloc] initWithFrame:viewFrame] retain];
statusMenuView.offset = aOffset;
statusItem.view = statusMenuView;
//setup the window to show when clicked
NSRect contentRect = NSZeroRect;
contentRect.size = aView.frame.size;
statusMenuWindow = [[[NSWindow alloc] initWithContentRect:contentRect
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO] retain];
[statusMenuWindow setLevel:NSPopUpMenuWindowLevel];
[statusMenuWindow setBackgroundColor:[NSColor clearColor]];
[statusMenuWindow setMovableByWindowBackground:NO];
[statusMenuWindow setExcludedFromWindowsMenu:YES];
[statusMenuWindow setOpaque:NO];
[statusMenuWindow setHasShadow:NO];
[statusMenuWindow useOptimizedDrawing:YES];
[[statusMenuWindow contentView] addSubview:aView];
[statusMenuWindow setDelegate:self];
statusMenuView.statusMenuWindow = statusMenuWindow;
And here is how I'm showing the window:
- (void)centerView{
NSRect menuFrame = self.window.frame;
NSRect windowFrame = self.statusMenuWindow.frame;
NSPoint menuPoint = NSMakePoint(NSMidX(menuFrame), NSMinY(menuFrame));
menuPoint.x -= windowFrame.size.width*0.5f;
menuPoint.y -= windowFrame.size.height+self.offset;
[self.statusMenuWindow setFrameOrigin:menuPoint];
[self.statusMenuWindow makeKeyAndOrderFront:self];
}
I was hoping the windowDidResignKey delegate method would do the trick but it doesn't go off with this configuration. The delegate is working because windowDidMove does run.
- (void)windowDidResignKey:(NSNotification *)notification{
NSLog(#"windowDidResignKey");
[statusMenuView hideView];
}
- (void)windowDidResignMain:(NSNotification *)notification{
NSLog(#"windowDidResignMain");
}
- (void)windowDidMove:(NSNotification *)notification{
NSLog(#"windowDidMove");
}
So to recap, how can I hide my custom window when the user clicks on anything else, the way the standard status bar menus work?
Edit
After looking at the Popup example the only thing I was missing was I had to subclass NSPanel and make it so it could become the key window.
#interface Panel : NSPanel
#end
#implementation Panel
- (BOOL)canBecomeKeyWindow{
return YES;
}
#end
You need to make sure your window can become the key window, and call your window's orderOut: method when it resigns key. You should be using a custom NSWindowController, if you are not already, in which case you would just call its "close" method to dismiss your window.
Instead of posting a bunch of code, I would suggest you just look at this excellent, recently-posted example of attaching a window to a status item:
Shpakovski Popup Window Example
I want to add functionality to my back buttons through my UINavigationController-based app where long-pressing the back button will pop to root. However, I can't figure out where to attach the gesture recognizer. Do I subclass UINavigationBar and try and detect if the long press is in the left button region?
I've heard of people adding similar functionality before. Anyone have any ideas?
I know this question is old, but I came up with a solution. Instead of trying to add the gesture recognizer to the button itself (which would be ideal), I added it to the self.navigationController.navigationBar and then in the action method, use the locationInView to see if I'm over the back button. I wasn't entirely sure about how to identify the back button precisely, so I'm clumsily just grabbing the the first subview with an x coordinate less than some arbitrary value, but it seems promising. If someone has a better way to identify the frame of the back button, let me know.
- (void)longPress:(UILongPressGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
// set a default rectangle in case we don't find the back button for some reason
CGRect rect = CGRectMake(0, 0, 100, 40);
// iterate through the subviews looking for something that looks like it might be the right location to be the back button
for (UIView *subview in self.navigationController.navigationBar.subviews)
{
if (subview.frame.origin.x < 30)
{
rect = subview.frame;
break;
}
}
// ok, let's get the point of the long press
CGPoint longPressPoint = [sender locationInView:self.navigationController.navigationBar];
// if the long press point in the rectangle then do whatever
if (CGRectContainsPoint(rect, longPressPoint))
[self doWhatever];
}
}
- (void)addLongPressGesture
{
if (NSClassFromString(#"UILongPressGestureRecognizer"))
{
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress:)];
[self.navigationController.navigationBar addGestureRecognizer:longPress];
[longPress release];
}
}
I believe UIGestureRecognizers can only be added to UIViews and subclasses of UIViews.
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIView_Class/UIView/UIView.html
The back button is a UIBarButtonItem that descends from NSObject. Therefore, you won't be able to attach a gesture recognizer to a standard back button using
UILongPressGestureRecognizer *longPressGesture =
[[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(longPress:)] autorelease];
[self.navigationItem.backBarButtonItem addGestureRecognizer:longPressGesture];
You can however add a custom view to a UIBarButtonItem. A custom view could just as easily be a UIView, UIButton, UILabel, etc.
Example:
UIView *myTransparentGestureView = [[UIView alloc] initWithFrame:CGRectMake(0,0,40,30)];
[myTransparentGestureView addGestureRecognizer:longPressGesture];
[self.navigationItem.backBarButtonItem setCustomView:myTransparentGestureView];
// Or you could set it like this
// self.navigationItem.backBarButtonItem.customView = myTransparentGestureView;
[myTransparentGestureView release];
You have to be careful however, since setting properties on backBarButtonItem applies to the next view that you push. So if you have view A that pushes to view B and you want the gesture to be recognized when you tap back in view B. You must set it up in view A.
I followed a slightly different path, figured I'd share it. The above answers are fine, but really, if the long press is in the leading 1/3 of the nav bar, that's good enough for me:
- (void)longPress:(UILongPressGestureRecognizer *)gr
{
NSLog(#"longPress:");
UINavigationBar *navBar = [self navigationBar];
CGFloat height = navBar.bounds.size.height;
CGPoint pt = [gr locationOfTouch:0 inView:navBar];
//NSLog(#"PT=%# height=%f", NSStringFromCGPoint(pt), height);
if(CGRectContainsPoint(CGRectMake(0,0,100,height), pt)) {
[self popToViewController:self.viewControllers[0] animated:YES];
}
}
Here's my solution:
In appDelegate (the "owner" of the nav bar in my app), In applicationDidFinishLaunchingWithOptions:
Get the nav bar view and add the gesture recognizer to the whole view:
// Get the nav bar view
UINavigationBar *myNavBar = nil;
for (UIView *view in [self.window.rootViewController.view subviews]) {
if ([view isKindOfClass:[UINavigationBar class]]) {
NSLog(#"Found Nav Bar!!!");
myNavBar = (UINavigationBar *)view;
}
}
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:#selector(backButtonLongPress:)];
[myNavBar addGestureRecognizer:longPress];
NSLog(#"Gesture Recognizer Added.");
Then in appDelegate, in -(void) backButtonLongPress:(id) sender
Check to see if the gesture occurs within the frame of the back button:
if ([sender state] == UIGestureRecognizerStateBegan) {
// Get the nav bar view
UINavigationBar *myNavBar = nil;
for (UIView *view in [self.window.rootViewController.view subviews]) {
if ([view isKindOfClass:[UINavigationBar class]]) {
NSLog(#"Found Nav Bar!!!");
myNavBar = (UINavigationBar *)view;
}
}
// Get the back button view
UIView *backButtonView = nil;
for (UIView *view in [myNavBar subviews]) {
if ([[[view class] description] isEqualToString:#"UINavigationItemButtonView"]) {
backButtonView = view;
NSLog(#"Found It: %#", backButtonView);
NSLog(#"Back Button View Frame: %f, %f; %f, %f", backButtonView.frame.origin.x, backButtonView.frame.origin.y, backButtonView.frame.size.width, backButtonView.frame.size.height);
}
}
CGPoint longPressPoint = [sender locationInView:myNavBar];
NSLog(#"Touch is in back button: %#", CGRectContainsPoint(backButtonView.frame, longPressPoint) ? #"YES" : #"NO");
if (CGRectContainsPoint(backButtonView.frame, longPressPoint)) {
// Place your action here
}
// Do nothing if outside the back button frame
}
Is there an apple-house-made way to get a UISlider with a ProgressView. This is used by many streaming applications e.g. native quicktimeplayer or youtube.
(Just to be sure: i'm only in the visualization interested)
cheers Simon
Here's a simple version of what you're describing.
It is "simple" in the sense that I didn't bother trying to add the shading and other subtleties. But it's easy to construct and you can tweak it to draw in a more subtle way if you like. For example, you could make your own image and use it as the slider's thumb.
This is actually a UISlider subclass lying on top of a UIView subclass (MyTherm) that draws the thermometer, plus two UILabels that draw the numbers.
The UISlider subclass eliminates the built-in track, so that the thermometer behind it shows through. But the UISlider's thumb (knob) is still draggable in the normal way, and you can set it to a custom image, get the Value Changed event when the user drags it, and so on. Here is the code for the UISlider subclass that eliminates its own track:
- (CGRect)trackRectForBounds:(CGRect)bounds {
CGRect result = [super trackRectForBounds:bounds];
result.size.height = 0;
return result;
}
The thermometer is an instance of a custom UIView subclass, MyTherm. I instantiated it in the nib and unchecked its Opaque and gave it a background color of Clear Color. It has a value property so it knows how much to fill the thermometer. Here's its drawRect: code:
- (void)drawRect:(CGRect)rect {
CGContextRef c = UIGraphicsGetCurrentContext();
[[UIColor whiteColor] set];
CGFloat ins = 2.0;
CGRect r = CGRectInset(self.bounds, ins, ins);
CGFloat radius = r.size.height / 2.0;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, CGRectGetMaxX(r) - radius, ins);
CGPathAddArc(path, NULL, radius+ins, radius+ins, radius, -M_PI/2.0, M_PI/2.0, true);
CGPathAddArc(path, NULL, CGRectGetMaxX(r) - radius, radius+ins, radius, M_PI/2.0, -M_PI/2.0, true);
CGPathCloseSubpath(path);
CGContextAddPath(c, path);
CGContextSetLineWidth(c, 2);
CGContextStrokePath(c);
CGContextAddPath(c, path);
CGContextClip(c);
CGContextFillRect(c, CGRectMake(r.origin.x, r.origin.y, r.size.width * self.value, r.size.height));
}
To change the thermometer value, change the MyTherm instance's value to a number between 0 and 1, and tell it to redraw itself with setNeedsDisplay.
This is doable using the standard controls.
In Interface Builder place your UISlider immediately on top of your UIProgressView and make them the same size.
On a UISlider the background horizontal line is called the track, the trick is to make it invisible. We do this with a transparent PNG and the UISlider methods setMinimumTrackImage:forState: and setMaximumTrackImage:forState:.
In the viewDidLoad method of your view controller add:
[self.slider setMinimumTrackImage:[UIImage imageNamed:#"transparent.png"] forState:UIControlStateNormal];
[self.slider setMaximumTrackImage:[UIImage imageNamed:#"transparent.png"] forState:UIControlStateNormal];
where self.slider refers to your UISlider.
I've tested the code in Xcode, and this will give you a slider with an independent progress bar.
Solution that suits my design:
class SliderBuffering:UISlider {
let bufferProgress = UIProgressView(progressViewStyle: .Default)
override init (frame : CGRect) {
super.init(frame : frame)
}
convenience init () {
self.init(frame:CGRect.zero)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
self.minimumTrackTintColor = UIColor.clearColor()
self.maximumTrackTintColor = UIColor.clearColor()
bufferProgress.backgroundColor = UIColor.clearColor()
bufferProgress.userInteractionEnabled = false
bufferProgress.progress = 0.0
bufferProgress.progressTintColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.5)
bufferProgress.trackTintColor = UIColor.blackColor().colorWithAlphaComponent(0.5)
self.addSubview(bufferProgress)
}
}
Create a UISlider:
// 1
// Make the slider as a public propriety so you can access it
playerSlider = [[UISlider alloc] init];
[playerSlider setContinuous:YES];
[playerSlider setHighlighted:YES];
// remove the slider filling default blue color
[playerSlider setMaximumTrackTintColor:[UIColor clearColor]];
[playerSlider setMinimumTrackTintColor:[UIColor clearColor]];
// Chose your frame
playerSlider.frame = CGRectMake(--- , -- , yourSliderWith , ----);
// 2
// create a UIView that u can access and make it the shadow of your slider
shadowSlider = [[UIView alloc] init];
shadowSlider.backgroundColor = [UIColor lightTextColor];
shadowSlider.frame = CGRectMake(playerSlider.frame.origin.x , playerSlider.frame.origin.y , playerSlider.frame.size.width , playerSlider.frame.origin.size.height);
shadowSlider.layer.cornerRadius = 4;
shadowSlider.layer.masksToBounds = YES;
[playerSlider addSubview:shadowSlider];
[playerSlider sendSubviewToBack:shadowSlider];
// 3
// Add a timer Update your slider and shadow slider programatically
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(updateSlider) userInfo:nil repeats:YES];
-(void)updateSlider {
// Update the slider about the music time
playerSlider.value = audioPlayer.currentTime; // based on ur case
playerSlider.maximumValue = audioPlayer.duration;
float smartWidth = 0.0;
smartWidth = (yourSliderFullWidth * audioPlayer.duration ) / 100;
shadowSlider.frame = CGRectMake( shadowSlider.frame.origin.x , shadowSlider.frame.origin.y , smartWidth , shadowSlider.frame.size.height);
}
Enjoy! P.S. I might have some typos.
Idea 1:
You could easily use the UISlider as a progress view by subclassing it. It responds to methods such as 'setValue:animated:' with which you can set the value (i.e: progress) of the view.
Your only 'restriction' creating what you see in your example is the buffer bar, which you could create by 'creatively' skinning the UISlider (because you can add custom skins to it), and perhaps set that skin programmatically.
Idea 2:
Another (easier) option is to subclass UIProgressView, and create a UISlider inside that subclass. You can skin the UISlider to have a see-through skin (no bar, just the knob visible) and lay it over the UIProgressView.
You can use the UIProgressView for the pre-loading (buffering) and the UISlider for movie control / progress indication.
Seems fairly easy :-)
Edit: to actually answer your question, there is no in-house way, but it would be easy to accomplish with the tools given.
You can do some trick like this, it's more easy and understanding. Just insert the code bellow in your UISlider subclass.
- (void)layoutSubviews
{
[super layoutSubviews];
if (_availableDurationImageView == nil) {
// step 1
// get max length that our "availableDurationImageView" will show
UIView *maxTrackView = [self.subviews objectAtIndex:self.subviews.count - 3];
UIImageView *maxTrackImageView = [maxTrackView.subviews objectAtIndex:0];
_maxLength = maxTrackImageView.width;
// step 2
// get the right frame where our "availableDurationImageView" will place in superView
UIView *minTrackView = [self.subviews objectAtIndex:self.subviews.count - 2];
_availableDurationImageView = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:#"MediaSlider.bundle/4_jindu_huancun.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 2, 0, 2)]];
_availableDurationImageView.opaque = NO;
_availableDurationImageView.frame = minTrackView.frame;
[self insertSubview:_availableDurationImageView belowSubview:minTrackView];
}
}
- (void)setAvailableValue:(NSTimeInterval)availableValue
{
if (availableValue >=0 && availableValue <= 1) {
// use "maxLength" and percentage to set our "availableDurationImageView" 's length
_availableDurationImageView.width = _maxLength * availableValue;
}
}
Adding on matt's solution, note that as of iOS 7.0, implementing trackRectForBounds: is rendered impossible. Here is my solution to this problem :
In your UISlider subclass, do this :
-(void)awakeFromNib
{
[super awakeFromNib];
UIImage* clearColorImage = [UIImage imageWithColor:[UIColor clearColor]];
[self setMinimumTrackImage:clearColorImage forState:UIControlStateNormal];
[self setMaximumTrackImage:clearColorImage forState:UIControlStateNormal];
}
with imageWithColor as this function :
+ (UIImage*) imageWithColor:(UIColor*)color
{
return [UIImage imageWithColor:color andSize:CGSizeMake(1.0f, 1.0f)];
}
That will properly take care of this annoying trackRectangle.
I spent too much time looking for a solution to this problem, here's hoping that'll save some time to another poor soul ;).
Here is a solution in Objective C. https://github.com/abhimuralidharan/BufferSlider
The idea is to create a UIProgressview as a property in the UISlider subclass and add the required constraints programatically.
#import <UIKit/UIKit.h> //.h file
#interface BufferSlider : UISlider
#property(strong,nonatomic) UIProgressView *bufferProgress;
#end
#import "BufferSlider.h" //.m file
#implementation BufferSlider
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
-(void)setup {
self.bufferProgress = [[UIProgressView alloc] initWithFrame:self.bounds];
self.minimumTrackTintColor = [UIColor redColor];
self.maximumTrackTintColor = [UIColor clearColor];
self.value = 0.2;
self.bufferProgress.backgroundColor = [UIColor clearColor];
self.bufferProgress.userInteractionEnabled = NO;
self.bufferProgress.progress = 0.7;
self.bufferProgress.progressTintColor = [[UIColor blueColor] colorWithAlphaComponent:0.5];
self.bufferProgress.trackTintColor = [[UIColor lightGrayColor] colorWithAlphaComponent:2];
[self addSubview:self.bufferProgress];
[self setThumbImage:[UIImage imageNamed:#"redThumb"] forState:UIControlStateNormal];
self.bufferProgress.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *left = [NSLayoutConstraint constraintWithItem:self.bufferProgress attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1 constant:0];
NSLayoutConstraint *centerY = [NSLayoutConstraint constraintWithItem:self.bufferProgress attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1 constant:0.75]; // edit the constant value based on the thumb image
NSLayoutConstraint *right = [NSLayoutConstraint constraintWithItem:self.bufferProgress attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTrailing multiplier:1 constant:0];
[self addConstraints:#[left,right,centerY]];
[self sendSubviewToBack:self.bufferProgress];
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self setup];
}
return self;
}
#end
for Swift5
First, add tap gesture to slider:
let tap_gesture = UITapGestureRecognizer(target: self, action: #selector(self.sliderTapped(gestureRecognizer:)))
self.slider.addGestureRecognizer(tap_gesture)
Then, implement this function:
#objc func sliderTapped(gestureRecognizer: UIGestureRecognizer) {
let locationOnSlider = gestureRecognizer.location(in: self.slider)
let maxWidth = self.slider.frame.size.width
let touchLocationRatio = locationOnSlider.x * CGFloat(self.slider.maximumValue) / CGFloat(maxWidth)
self.slider.value = Float(touchLocationRatio)
print("New value: ", round(self.slider.value))
}