Cache decoded UIImage objects for the scroll view to fix janky scrolling

This commit is contained in:
Cameron Gutman
2015-11-10 21:23:00 -08:00
parent c8441ec9fa
commit 20d66336a7
3 changed files with 104 additions and 21 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
@interface UIAppView : UIView @interface UIAppView : UIView
- (id) initWithApp:(App*)app andCallback:(id<AppCallback>)callback; - (id) initWithApp:(App*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback;
- (void) updateAppImage; - (void) updateAppImage;
@end @end
+37 -14
View File
@@ -13,36 +13,35 @@
UIButton* _appButton; UIButton* _appButton;
UILabel* _appLabel; UILabel* _appLabel;
UIImageView* _appOverlay; UIImageView* _appOverlay;
NSCache* _artCache;
id<AppCallback> _callback; id<AppCallback> _callback;
} }
- (id) initWithApp:(App*)app andCallback:(id<AppCallback>)callback { static UIImage* noImage;
- (id) initWithApp:(App*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback {
self = [super init]; self = [super init];
_app = app; _app = app;
_callback = callback; _callback = callback;
_artCache = cache;
// Cache the NoAppImage ourselves to avoid
// having to load it each time
if (noImage == nil) {
noImage = [UIImage imageNamed:@"NoAppImage"];
}
_appButton = [UIButton buttonWithType:UIButtonTypeCustom]; _appButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage* noImage = [UIImage imageNamed:@"NoAppImage"];
[_appButton setBackgroundImage:noImage forState:UIControlStateNormal]; [_appButton setBackgroundImage:noImage forState:UIControlStateNormal];
[_appButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)]; [_appButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
[_appButton sizeToFit]; [_appButton sizeToFit];
[_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside]; [_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
_appOverlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Play"]];
_appOverlay.layer.shadowColor = [UIColor blackColor].CGColor;
_appOverlay.layer.shadowOffset = CGSizeMake(0, 0);
_appOverlay.layer.shadowOpacity = 1;
_appOverlay.layer.shadowRadius = 2.0;
[_appOverlay setHidden: YES];
[self addSubview:_appButton]; [self addSubview:_appButton];
[self addSubview:_appOverlay];
[self sizeToFit]; [self sizeToFit];
_appButton.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height); _appButton.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height);
_appOverlay.frame = CGRectMake(0, 0, noImage.size.width / 2.f, noImage.size.height / 4.f);
self.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height); self.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height);
[_appOverlay setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/6)];
// Rasterizing the cell layer increases rendering performance by quite a bit // Rasterizing the cell layer increases rendering performance by quite a bit
self.layer.shouldRasterize = YES; self.layer.shouldRasterize = YES;
@@ -56,13 +55,37 @@
} }
- (void) updateAppImage { - (void) updateAppImage {
[_appOverlay setHidden:!_app.isRunning]; if (_appOverlay != nil) {
[_appOverlay removeFromSuperview];
_appOverlay = nil;
}
if (_app.isRunning) {
// Only create the app overlay if needed
_appOverlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Play"]];
_appOverlay.layer.shadowColor = [UIColor blackColor].CGColor;
_appOverlay.layer.shadowOffset = CGSizeMake(0, 0);
_appOverlay.layer.shadowOpacity = 1;
_appOverlay.layer.shadowRadius = 2.0;
[self addSubview:_appOverlay];
_appOverlay.frame = CGRectMake(0, 0, noImage.size.width / 2.f, noImage.size.height / 4.f);
[_appOverlay setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/6)];
}
// TODO: Improve no-app image detection // TODO: Improve no-app image detection
BOOL noAppImage = false; BOOL noAppImage = false;
if (_app.image != nil) { if (_app.image != nil) {
UIImage* appImage = [UIImage imageWithData:_app.image]; // Load the decoded image from the cache
UIImage* appImage = [_artCache objectForKey:_app];
if (appImage == nil) {
// Not cached; we have to decode this now
appImage = [UIImage imageWithData:_app.image];
[_artCache setObject:appImage forKey:_app];
}
// This size of image might be blank image received from GameStream. // This size of image might be blank image received from GameStream.
if (!(appImage.size.width == 130.f && appImage.size.height == 180.f)) { if (!(appImage.size.width == 130.f && appImage.size.height == 180.f)) {
_appButton.frame = CGRectMake(0, 0, appImage.size.width / 2, appImage.size.height / 2); _appButton.frame = CGRectMake(0, 0, appImage.size.width / 2, appImage.size.height / 2);
@@ -5,6 +5,8 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved. // Copyright (c) 2014 Moonlight Stream. All rights reserved.
// //
@import ImageIO;
#import "MainFrameViewController.h" #import "MainFrameViewController.h"
#import "CryptoManager.h" #import "CryptoManager.h"
#import "HttpManager.h" #import "HttpManager.h"
@@ -38,6 +40,7 @@
UIScrollView* hostScrollView; UIScrollView* hostScrollView;
int currentPosition; int currentPosition;
NSArray* _sortedAppList; NSArray* _sortedAppList;
NSCache* _boxArtCache;
} }
static NSMutableSet* hostList; static NSMutableSet* hostList;
@@ -195,6 +198,10 @@ static NSMutableSet* hostList;
} }
- (void) receivedAssetForApp:(App*)app { - (void) receivedAssetForApp:(App*)app {
// Update the box art cache now so we don't have to do it
// on the main thread
[self updateBoxArtCacheForApp:app];
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView reloadData]; [self.collectionView reloadData];
}); });
@@ -500,6 +507,8 @@ static NSMutableSet* hostList;
hostList = [[NSMutableSet alloc] init]; hostList = [[NSMutableSet alloc] init];
} }
_boxArtCache = [[NSCache alloc] init];
[self setAutomaticallyAdjustsScrollViewInsets:NO]; [self setAutomaticallyAdjustsScrollViewInsets:NO];
hostScrollView = [[ComputerScrollView alloc] init]; hostScrollView = [[ComputerScrollView alloc] init];
@@ -524,10 +533,28 @@ static NSMutableSet* hostList;
[self retrieveSavedHosts]; [self retrieveSavedHosts];
_discMan = [[DiscoveryManager alloc] initWithHosts:[hostList allObjects] andCallback:self]; _discMan = [[DiscoveryManager alloc] initWithHosts:[hostList allObjects] andCallback:self];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(handleReturnToForeground)
name: UIApplicationWillEnterForegroundNotification
object: nil];
[self updateHosts]; [self updateHosts];
[self.view addSubview:hostScrollView]; [self.view addSubview:hostScrollView];
} }
-(void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(void)handleReturnToForeground
{
// This will refresh the applist
if (_selectedHost != nil) {
[self hostClicked:_selectedHost view:nil];
}
}
- (void)viewDidAppear:(BOOL)animated - (void)viewDidAppear:(BOOL)animated
{ {
[super viewDidAppear:animated]; [super viewDidAppear:animated];
@@ -540,10 +567,7 @@ static NSMutableSet* hostList;
[_discMan startDiscovery]; [_discMan startDiscovery];
// This will refresh the applist [self handleReturnToForeground];
if (_selectedHost != nil) {
[self hostClicked:_selectedHost view:nil];
}
} }
- (void)viewDidDisappear:(BOOL)animated - (void)viewDidDisappear:(BOOL)animated
@@ -552,6 +576,9 @@ static NSMutableSet* hostList;
// when discovery stops, we must create a new instance because you cannot restart an NSOperation when it is finished // when discovery stops, we must create a new instance because you cannot restart an NSOperation when it is finished
[_discMan stopDiscovery]; [_discMan stopDiscovery];
// Purge the box art cache
[_boxArtCache removeAllObjects];
// In case the host objects were updated in the background // In case the host objects were updated in the background
[[[DataManager alloc] init] saveData]; [[[DataManager alloc] init] saveData];
} }
@@ -605,6 +632,7 @@ static NSMutableSet* hostList;
[hostScrollView addSubview:compView]; [hostScrollView addSubview:compView];
} }
} }
prevEdge = [self getCompViewX:addComp addComp:addComp prevEdge:prevEdge]; prevEdge = [self getCompViewX:addComp addComp:addComp prevEdge:prevEdge];
addComp.center = CGPointMake(prevEdge, hostScrollView.frame.size.height / 2); addComp.center = CGPointMake(prevEdge, hostScrollView.frame.size.height / 2);
@@ -620,6 +648,30 @@ static NSMutableSet* hostList;
} }
} }
// This function forces immediate decoding of the UIImage, rather
// than the default lazy decoding that results in janky scrolling.
+ (UIImage*) loadBoxArtForCaching:(App*)app {
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)app.image, NULL);
CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)@{(id)kCGImageSourceShouldCacheImmediately: (id)kCFBooleanTrue});
UIImage *boxArt = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
CFRelease(source);
return boxArt;
}
- (void) updateBoxArtCacheForApp:(App*)app {
if (app.image == nil) {
[_boxArtCache removeObjectForKey:app];
}
else if ([_boxArtCache objectForKey:app] == nil) {
[_boxArtCache setObject:[MainFrameViewController loadBoxArtForCaching:app] forKey:app];
}
}
- (void) updateAppsForHost:(Host*)host { - (void) updateAppsForHost:(Host*)host {
if (host != _selectedHost) { if (host != _selectedHost) {
Log(LOG_W, @"Mismatched host during app update"); Log(LOG_W, @"Mismatched host during app update");
@@ -629,6 +681,15 @@ static NSMutableSet* hostList;
_sortedAppList = [host.appList allObjects]; _sortedAppList = [host.appList allObjects];
_sortedAppList = [_sortedAppList sortedArrayUsingSelector:@selector(compareName:)]; _sortedAppList = [_sortedAppList sortedArrayUsingSelector:@selector(compareName:)];
// Start populating the box art cache asynchronously
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
Log(LOG_I, @"Starting per-computer box art caching job");
for (App* app in host.appList) {
[self updateBoxArtCacheForApp:app];
}
Log(LOG_I, @"Per-computer box art caching job completed");
});
[hostScrollView removeFromSuperview]; [hostScrollView removeFromSuperview];
[self.collectionView reloadData]; [self.collectionView reloadData];
} }
@@ -636,9 +697,8 @@ static NSMutableSet* hostList;
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"AppCell" forIndexPath:indexPath]; UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"AppCell" forIndexPath:indexPath];
App* app = _sortedAppList[indexPath.row]; App* app = _sortedAppList[indexPath.row];
UIAppView* appView = [[UIAppView alloc] initWithApp:app andCallback:self]; UIAppView* appView = [[UIAppView alloc] initWithApp:app cache:_boxArtCache andCallback:self];
[appView updateAppImage]; [appView updateAppImage];
if (appView.bounds.size.width > 10.0) { if (appView.bounds.size.width > 10.0) {