UIScrollView lazy loading of images to reduce memory usage and avoid crash - objective-c

My app, using scrollview that loads multiple images with NSOperation (Max around 100sh). I tried to test it out on my ipod 2Gen and it crashes due to low memory on device, but works fine on ipod 4th Gen. On 2nd Gen, it crashes when it loads about 15-20 images. How should I handle this problem ?

You could load you images lazily. That means, e.g., just a couple of images at a time in your scroll view, so that you can animate to the next and the previous one; when you move to the right, e.g., you also load one more image; at the same time, you unload images that are not directly accessible anymore (e.g. those that have remained to the left).
You should make the number of preloaded image sufficiently high so that the user can scroll without waiting at any time; this also depends on how big those images are and where they come from (i.e., how long it takes to load them)... a good starting point would be, IMO, 5 images loaded at any time.
Here you will find a nice step by step tutorial.
EDIT:
Since the link above seems to be broken, here is the final code from that post:
-(void)scrollViewDidScroll:(UIScrollView *)myScrollView {
/**
* calculate the current page that is shown
* you can also use myScrollview.frame.size.height if your image is the exact size of your scrollview
*/
int currentPage = (myScrollView.contentOffset.y / currentImageSize.height);
// display the image and maybe +/-1 for a smoother scrolling
// but be sure to check if the image already exists, you can do this very easily using tags
if ( [myScrollView viewWithTag:(currentPage +1)] ) {
return;
}
else {
// view is missing, create it and set its tag to currentPage+1
}
/**
* using your paging numbers as tag, you can also clean the UIScrollView
* from no longer needed views to get your memory back
* remove all image views except -1 and +1 of the currently drawn page
*/
for ( int i = 0; i < currentPages; i++ ) {
if ( (i < (currentPage-1) || i > (currentPage+1)) && [myScrollView viewWithTag:(i+1)] ) {
[[myScrollView viewWithTag:(i+1)] removeFromSuperview];
}
}
}

Related

How to step one frame forward and one frame backward in video playback?

I need to search for some special features/patterns that might be visible in only one (or two) of many frames. The frame rate can be as slow as 25 frames per second and the video may contain over 7500 frames. I often start by scanning the video at high speed looking for a segment where I might find the feature, then rewind. I repeat this procedure while gradually reducing the playback speed, until I find a fairly small time window in which I can expect to find the feature (if it is present). I would then like to step forward and backward by single frames using key hit events (e.g. right arrow and left arrow keys) to find the feature of interest. I have managed to use HTML5 with JavaScript to control the forward speed; but, still do not know how to use the keyboard for single frame stepping forward and backward through a video. How can this be accomplished? Note, my web browser is Firefox 26 running on a Windows 7 platform.
You can seek to any time in the video by setting the currentTime property. Something like this:
var video = document.getElementById('video'),
frameTime = 1 / 25; //assume 25 fps
window.addEventListener('keypress', function (evt) {
if (video.paused) { //or you can force it to pause here
if (evt.keyCode === 37) { //left arrow
//one frame back
video.currentTime = Math.max(0, video.currentTime - frameTime);
} else if (evt.keyCode === 39) { //right arrow
//one frame forward
//Don't go past the end, otherwise you may get an error
video.currentTime = Math.min(video.duration, video.currentTime + frameTime);
}
}
});
Just a couple things you need to be aware of, though they shouldn't cause you too much trouble:
There is no way to detect the frame rate, so you have to either hard-code it or list it in some lookup table or guess.
Seeking may take a few milliseconds or more and does not happen synchronously. The browser needs some time to load the video from the network (if it's not already loaded) and to decode the frame. If you need to know when seeking is done, you can listen for the 'seeked' event.
You can check to see if the video has advanced to the next frame by checking the
targetTime += (1 / 25) // 25 fps
video.currentTime = targetTime // set the video frame
if (video.currentTime >= video.duration) { // if it's the end of the video
alert('done!');
}
if (video.currentTime == targetTime) { // frame has been updated
}

UICollectionView with preview and paging enabled

I am trying to imitate what Apple has when showing the search result in the App Store. (reference: http://searchengineland.com/apple-app-search-shows-only-one-result-at-a-time-133818)
It shows like the detailed-application-info in a cards and it is paged. I am stuck at how to make the previous-and-next card shows when one active card in the middle and the scroll view's paging behaviour is still intact.
I have tried using the UICollectionView and set the clipSubviews to NO, hoping that it will show the previous page and the next page, but as soon as the cell goes off-screen, the cell gets hidden (removed from the view hierarchy) and not displayed. I think thats the flyweight pattern of the UICollectionView (the behavior of UICollectionView). Any ideas of what would be possible?
Cheers,
Rendy Pranata
The problem: UICollectionView as a subclass of UIScrollView essentially animates its bounds by a stride of bounds.size. Although this could mean that all you had to do is decrease the bounds while keeping the frame bigger, unfortunately UICollectionView will not render any cells outside its current bounds... destroying your preview effect.
The Solution:
Create a UICollectionView with paging set to NO and with the desired frame.
Create UICollectionViewCells that are smaller than the UICollectionView's frame/bounds. At this stage, a part of the next cell should show in the frame. This should be visible before implementing the other steps below.
Add a collectionView.contentInset.left and right (I assume your layout is horizontal) equal to the contentOffsetValue method (as shown below for simplicity) so as to align the first and last cells to the middle.
Create a UICollectionViewFlowLayout which overrides the method that gives the stopping point like so:
Like so:
-(CGFloat)contentOffsetValue
{
return self.collectionView.bounds.size.width * 0.5f - self.itemSize.width * 0.5f;
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
static float EscapeVelocity = 0.5f; // otherwise snap back to the middle
NSArray* layoutAttributesArray = [self layoutAttributesForElementsInRect:self.collectionView.bounds];
if(layoutAttributesArray.count == 0)
return proposedContentOffset;
CGFloat currentBoundsCenterX = self.collectionView.contentOffset.x + self.collectionView.bounds.size.width * 0.5f;
UICollectionViewLayoutAttributes* candidateNextLayoutAttributes = layoutAttributesArray.firstObject;
for (UICollectionViewLayoutAttributes* layoutAttributes in layoutAttributesArray)
{
if ((layoutAttributes.representedElementCategory != UICollectionElementCategoryCell) ||
(layoutAttributes == candidateNextLayoutAttributes)) // skip the first comparison
continue;
if(velocity.x > EscapeVelocity || velocity.x < -(EscapeVelocity))
{
if(velocity.x > EscapeVelocity && layoutAttributes.center.x > candidateNextLayoutAttributes.center.x)
{
candidateNextLayoutAttributes = layoutAttributes;
}
else if (velocity.x < -(EscapeVelocity) && layoutAttributes.center.x < candidateNextLayoutAttributes.center.x)
{
candidateNextLayoutAttributes = layoutAttributes;
}
}
else
{
if(fabsf(currentBoundsCenterX - layoutAttributes.center.x) < fabsf(currentBoundsCenterX - candidateNextLayoutAttributes.center.x))
{
candidateNextLayoutAttributes = layoutAttributes;
}
}
}
return CGPointMake(candidateNextLayoutAttributes.center.x - self.collectionView.bounds.size.width * 0.5f, proposedContentOffset.y);
}
I just put together a sample project which shows how you could do this. I created a container view which is 100 points wider than the 320 points for the screen. Then I put a UICollectionView into that container. This offsets everything by 50 points on both sides of the screen.
Then there is a content cell which simply has a background and a label so you can visually identify what is happening. On the left and right there are empty cells. In the viewDidLoad method the content inset is set to negative values on the left and right to make the empty cells now scroll into view. You can adjust the inset to your preference.
This mimics the behavior fairly closely. To get the label below, like in the example you can simply check the contentOffset value to determine which cell is in focus. To do that you'd use the UIScrollViewDelegate which is a part of UICollectionView.
https://github.com/brennanMKE/Interfaces/tree/master/ListView
You'll notice this sample project has 2 collection views. One is a normal horizontal flow layout while the other one which has larger cells is the one which mimics the example you mentioned.

How to add and delete a view in UIscrollview at runtime?

My project needs to add and delete a uiwebview to uiscrollview at runtime. Means, when we scroll it horizontally (left or right) when paging is enabled, then a new view get added to the uiscrollview and it traverse to it.
Is it possible to detect the left or right scrolling in uiscrollview?
Plz tell me the best possible solution, sample code or any tutorial.
Thanks in advance
In such cases, we should have paging enabled in our scroll view.
Lets say you have scroll view of size 320x480, and it is supposed to show 10 pages, where size of each page is 320x480, making content size of scroll view as 320*10 x 480.
The best way to determine the current page is by using the content offset value of the scroll view.
So, in the beginning, when scrollview is showing 1st page, its content offset would be x=0, y=0.
For 2nd page x=320, y=0.
Thus we can get the current page value by dividing the contentOffset.x by page-width.
Thus, 0/320 = 0, means 1st page. 320/320 = 1, means 2nd page, and so on.
Thus, if we have current page value with us, we can determine in which direction the scroll view is moving, as follows:
-(void) scrollViewDidScroll:(UIScrollView *)scrollView{
int currentPageOffset = currentPage * PAGE_WIDTH;
if (self.pageScrollView.contentOffset.x >= currentPageOffset + PAGE_WIDTH) {
// Scroll in right direction. Reached the next page offset.
// Settings for loading the next page.
currentPage = self.pageScrollView.contentOffset.x/PAGE_WIDTH;
}else if (self.pageScrollView.contentOffset.x <= currentPageOffset - PAGE_WIDTH) {
// Scroll in left direction. Reached the previous page offset.
// Setting for loading the previous page.
currentPage = self.pageScrollView.contentOffset.x/PAGE_WIDTH;
}
}

Delay url ImageView load until view is on screen?

I'm loading many images from a server, and can't hammer the server with all those requests at once. I want to only load the images once they've scrolled into view on screen, similar to the Facebook iPhone app.
Anybody know how to do this in Titanium?
I think I've seen a clue in the Kitchen Sink / YQL demo: It appears that a table will provide this functionality to the ImageView. Am going to test it...
the table will only load the images for the cells once the row is visible.
Edit: Ignore below, it seems Appcelerator doesn't wait until the images are rendered before requesting the image data. Sounds like a bug.
That sounds right to me. A table (on iOS, at least) will lazy-load its cells, not processing them until they are on the screen. Rather than adding loads of images to a view, if you create table cells instead and insert the images into them they'll be loaded on demand. You can always style the table so it doesn't look like they're in a table, if you don't want that effect.
var table = Ti.UI.createTableView({
separatorColor: '#FFF' // Use this to hide the separator if you don't want it to look like a table
});
var images = []; // This contains your image views
var data = [];
for(var i=0, n=images.length; i<n; i++) {
var cell = Ti.UI.createTableViewRow({
className: 'image' // boosts performance
});
cell.add(images[i]);
data.push(cell);
}
table.data = data;
// Then just add the table to your window

while I scroll between the layout it takes too long to be able to scroll between the gallerie's pictures. Is there any way to reduce this time?

this is my first question here, though I've being reading this forum for quite a while. Most of the answers to my doubts are from here :)
Getting back on topic. I'm developing an Android application. I'm drawing a dynamic layout that are basically Galleries, inside a LinearLayout, inside a ScrollView, inside a RelativeLayout. The ScrollView is a must, because I'm drawing a dynamic amount of galleries that most probably will not fit on the screen.
When I scroll inside the layout, I have to wait 3/4 seconds until the ScrollView "deactivates" to be able to scroll inside the galleries. What I want to do is to reduce this time to a minimum. Preferably I would like to be able to scroll inside the galleries as soon as I lift my finger from the screen, though anything lower than 2 seconds would be great as well.
I've being googling around for a solution but all I could find until now where layout tutorials that didn't tackle this particular issue. I was hoping someone here knows if this is possible and if so to give me some hints on how to do so.
I would prefer not to do my own ScrollView to solve this. But if that is the only way I would appreciate some help because I'm not really sure how would I solve this issue by doing that.
this is my layout:
public class PicturesL extends Activity implements OnClickListener,
OnItemClickListener, OnItemLongClickListener {
private ArrayList<ImageView> imageView = new ArrayList<ImageView>();
private StringBuilder PicsDate = new StringBuilder();
private CaWaApplication application;
private long ListID;
private ArrayList<Gallery> gallery = new ArrayList<Gallery>();
private ArrayList<Bitmap> Thumbails = new ArrayList<Bitmap>();
private String idioma;
private ArrayList<Long> Days = new ArrayList<Long>();
private long oldDay;
private long oldThumbsLoaded;
private ArrayList<Long> ThumbailsDays = new ArrayList<Long>();
private ArrayList<ArrayList<Long>> IDs = new ArrayList<ArrayList<Long>>();
#Override
public void onCreate(Bundle savedInstancedState) {
super.onCreate(savedInstancedState);
RelativeLayout layout = new RelativeLayout(this);
ScrollView scroll = new ScrollView(this);
LinearLayout realLayout = new LinearLayout(this);
ArrayList<TextView> texts = new ArrayList<TextView>();
Button TakePic = new Button(this);
idioma = com.mateloft.cawa.prefs.getLang(this);
if (idioma.equals("en")) {
TakePic.setText("Take Picture");
} else if (idioma.equals("es")) {
TakePic.setText("Sacar Foto");
}
RelativeLayout.LayoutParams scrollLP = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.FILL_PARENT,
RelativeLayout.LayoutParams.FILL_PARENT);
layout.addView(scroll, scrollLP);
realLayout.setOrientation(LinearLayout.VERTICAL);
realLayout.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT));
scroll.addView(realLayout);
TakePic.setId(67);
TakePic.setOnClickListener(this);
application = (CaWaApplication) getApplication();
ListID = getIntent().getExtras().getLong("listid");
getAllThumbailsOfID();
LinearLayout.LayoutParams TakeLP = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
realLayout.addView(TakePic);
oldThumbsLoaded = 0;
int galler = 100;
for (int z = 0; z < Days.size(); z++) {
ThumbailsManager croppedThumbs = new ThumbailsManager(Thumbails,
oldThumbsLoaded,
ThumbailsDays.get(z));
oldThumbsLoaded = ThumbailsDays.get(z);
texts.add(new TextView(this));
texts.get(z).setText("Day " + Days.get(z).toString());
gallery.add(new Gallery(this));
gallery.get(z).setAdapter(new ImageAdapter(this, croppedThumbs.getGallery(), 250, 175, true,
ListID));
gallery.get(z).setOnItemClickListener(this);
gallery.get(z).setOnItemLongClickListener(this);
gallery.get(z).setId(galler);
galler++;
realLayout.addView(texts.get(z));
realLayout.addView(gallery.get(z));
}
Log.d("PicturesL", "ListID: " + ListID);
setContentView(layout);
}
private void getAllThumbailsOfID() {
ArrayList<ModelPics> Pictures = new ArrayList<ModelPics>();
ArrayList<String> ThumbailsPath = new ArrayList<String>();
Pictures = application.dataManager.selectAllPics();
long thumbpathloaded = 0;
int currentID = 0;
for (int x = 0; x < Pictures.size(); x++) {
if (Pictures.get(x).walkname == ListID) {
if (Days.size() == 0) { Days.add(Pictures.get(x).day); oldDay = Pictures.get(x).day;
IDs.add(new ArrayList<Long>()); currentID = 0; }
if (oldDay != Pictures.get(x).day) {
oldDay = Pictures.get(x).day;
ThumbailsDays.add(thumbpathloaded);
Days.add(Pictures.get(x).day);
IDs.add(new ArrayList<Long>());
currentID++;
}
StringBuilder tpath = new StringBuilder();
tpath.append(Pictures.get(x).path.substring(0,
Pictures.get(x).path.length() - 4));
tpath.append("-t.jpg");
IDs.get(currentID).add(Pictures.get(x).id);
ThumbailsPath.add(tpath.toString());
thumbpathloaded++;
if (x == Pictures.size() - 1) {
Log.d("PicturesL", "El ultimo de los arrays, tamaƱo: " + Days.size());
ThumbailsDays.add(thumbpathloaded);
}
}
}
for (int y = 0; y < ThumbailsPath.size(); y++) {
Thumbails.add(BitmapFactory.decodeFile(ThumbailsPath.get(y)));
}
}
I had a memory leak on another activity when screen orientation changed that was making it slower, now it is working better. The scroller is not locking up. But sometimes, when it stops scrolling, it takes a few seconds (2/3) to disable itself. I just want it to be a little more dynamic, is there any way to override the listener and make it stop scrolling ON_ACTION_UP or something like that?
I don't want to use the listview because I want to have each gallery separated by other views, now I just have text, but I will probably separate them with images with a different size than the galleries.
I'm not really sure if this is possible with a listadapter and a listview, I assumed that a view can only handle only one type of object, so I'm using a scrollview of a layout, if I'm wrong please correct me :)
Also this activity works as a preview or selecting the pictures you want to view in full size and manage their values. So its working only with thumbnails. Each one weights 40 kb. Guessing that is very unlikely that a user gets more than 1000~1500 pictures in this view, i thought that the activity wouldn't use more than 40~50 mb of ram in this case, adding 10 more if I open the fullsized view. So I guessed as well most devices are able to display this view in full size. If it doesn't work on low-end devices my plan was to add an option in the app preferences to let user chop this view according to some database values.
And a last reason is that during most of this activity "life-cycle" (the app has pics that are relevant to the view, when it ends the value that selects which pictures are displayed has to change and no more pictures are added inside this instance of this activity); the view will be unpopulated, so most of the time showing everything wont cost much, just at the end of its cycle
That was more or less what I thought at the time i created this layout. I'm open to any sort of suggestion or opinion, I just created this layout a few days ago and I'm trying to see if it can work right, because it suits my app needs. Though if there is a better way i would love to hear it
PD: I've being toying to see how much it can actually draw. I managed to draw 530 galleries with 600 thumbnails on the same layout, the app is using 42 mb, and I don't have access to more memory to start my full sized view afterwards :( .I tried to do the same with 1000 but it throws OutOfMemory error, bitmap size exceeds vm budget. Is there any way to make more memory availeable on high-end devices? Or should I try to figure out a way not to draw everything at once? Would the listview work for more than one type of object?
Thanks
Mateo
Why not make your galleries items of a listview? That way you won't have to inflate all of them immediately and can the advantage of the listview recyling mechanism. Some of the slowness you are seeing might be coming from very heavy memory usage. Galleries aren't exactly light widgets so you might be seeing pauses from heavy GC activity once you stop scrolling. ListView with an adapter would also allow you to lazily bind the Galleries to the list, such that during a fling you aren't doing the expensive binding of the galleries.