Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
Virtual display container
-
Has anyone written a custom class around a scrollview or other ui element that acts as a type of virtual display container? Something like you specify the a height and width of the virtual item, it could calculate the items per row or you could override it. Then via a delegate or whatever mechanism you get notified when your virtual item is on screen. Also to give like a row value for both behind and below the visible virtual items as a read ahead/back buffer. Yes, I know this is sounding like the tableView. Same same but different.
Also if the class could be polled to find out the virtual items that are outside the visible area + the read ahead/behind virtual items. This could allow for some freeing of resources.
I have about 1,000 pictures (movie posters) I want to display in a scroll view. I have got it working, but super slow and memory hungry, as well as not scalable.
I am asking the question because I would be surprised if someone has not written something like this before. Or possibly, there is a simple solution already built in I don't understand.
You could also ask why I don't write it myself. Well that would be a flattering question, the truth is, I am just not good enough yet.
One final question. It's about the content_view of the scrollview. Is there a bitmap or equivalent being created by ui that Is the size of the scrollview content_size. If so, it makes it difficult to make a scalable solution with scrollview I guessAny ideas appreciated.
-
This may be a starting point... It's basically a scroll view with a grid of cells. Each cell is rendered by a view, but views are only created for cells that are actually visible. When you scroll, so that some cells become invisible, their views are removed automatically, and they're "recycled" for new cells that move into the visible area.
Similar to a
TableView
, theGridView
has adata_source
that's responsible for creating and configuring cell views via two separate methods.The demo shows a scrollable grid of 9999 random colors, and it runs pretty smoothly. I hope the code makes sense to you. As I said, it's just a starting point...
import ui import math class GridView (ui.View): def __init__(self, *args, **kwargs): ui.View.__init__(self, *args, **kwargs) self.visible_range = [] self.visible_views = {} self.items = [] self.reusable_cells = [] self.item_size = (120, 120) self.scrollview = ui.ScrollView(frame=self.bounds, flex='WH') self.scrollview.content_size = (0, 2000) self.scrollview.delegate = self self.data_source = None self.add_subview(self.scrollview) def reload(self): self.visible_range = [] for v in self.visible_views.values(): self.scrollview.remove_subview(v) self.visible_views = {} w, h = self.bounds[2:] items_per_row = int(w / self.item_size[0]) num_rows = math.ceil(len(self.items) / float(items_per_row)) self.scrollview.content_size = (0, num_rows * self.item_size[1]) self.scrollview_did_scroll(self.scrollview) def layout(self): self.reload() def frame_for_item(self, item_index): w, h = self.bounds[2:] items_per_row = int(w / self.item_size[0]) row = item_index / items_per_row col = item_index % items_per_row x_spacing = (w - (items_per_row * self.item_size[0])) / (items_per_row-1) return (col*(self.item_size[0] + x_spacing), row*self.item_size[1], self.item_size[0], self.item_size[1]) def create_or_reuse_cell(self): if self.reusable_cells: cell = self.reusable_cells[0] del self.reusable_cells[0] return cell if self.data_source: return self.data_source.gridview_create_cell(self) else: return ui.View(bg_color='gray') def configure_cell(self, cell_view, item): if self.data_source: self.data_source.gridview_configure_cell(self, cell_view, item) def scrollview_did_scroll(self, scrollview): y = scrollview.content_offset[1] w, h = self.bounds[2:] items_per_row = int(w / self.item_size[0]) first_visible_row = max(0, int(y / self.item_size[1])) num_visible_rows = int(h / self.item_size[1]) + 2 range_start = first_visible_row * items_per_row range_end = min(len(self.items), range_start + num_visible_rows * items_per_row) visible_range = range(range_start, range_end) if visible_range != self.visible_range: self.visible_range = visible_range # Remove views that are no longer visible: for i in self.visible_views.keys(): if i not in visible_range: cell = self.visible_views[i] self.reusable_cells.append(cell) self.scrollview.remove_subview(cell) del self.visible_views[i] # Add views that are not visible yet: for i in visible_range: if i not in self.visible_views: cell_frame = self.frame_for_item(i) view = self.create_or_reuse_cell() view.frame = cell_frame self.configure_cell(view, self.items[i]) self.scrollview.add_subview(view) self.visible_views[i] = view class GridViewDemoController (object): def __init__(self): from random import randint # Generate a large number of random colors: colors = ['#%02x%02x%02x' % (randint(0, 255), randint(0, 255), randint(0, 255)) for i in xrange(9999)] self.gridview = GridView(frame=(0, 0, 500, 500), background_color='white', name='GridView Demo') self.gridview.item_size = (100, 120) self.gridview.data_source = self self.gridview.items = colors # Data source methods: def gridview_create_cell(self, gridview): # This is called when a new cell is needed. # When the grid view is scrolled, cells that become invisible are reused, # so this doesn't get called too often. cell = ui.View(frame=(0, 0, 100, 100)) swatch = ui.View(frame=(10, 10, 80, 60), name='swatch', flex='wh') swatch.corner_radius = 4 cell.add_subview(swatch) label = ui.Label(frame=(10, 80, 80, 15), name='label', flex='wt') label.font = ('<System>', 13) label.alignment = ui.ALIGN_CENTER label.text_color = '#333333' cell.add_subview(label) return cell def gridview_configure_cell(self, gridview, cell, item): # Note: The cell may be a new one (created by gridview_create_cell), # or an existing cell that is reused after it was scrolled out of the visible area. # This method should configure the cell to display the given item (which can be # any kind of object; in this demo, all items are strings). cell['label'].text = item cell['swatch'].background_color = item demo = GridViewDemoController() demo.gridview.present('sheet')
-
Thanks @omz...I will try. Good for me to have a starting point. My other question about ui and the bitmap behind, seems like is not an issue. I did the following stupid code, as far as I can see no memory loss and its instant.
import ui class VirtualScroll(ui.View): def __init__(self,frame, width , height, items_per_line , num_items): self.frame = frame # make the scroll view scroll = ui.ScrollView() scroll.frame = frame scroll.content_size = (self.width, height * num_items) print scroll.content_size self.add_subview(scroll) if __name__ == '__main__': frame = (0,0,540,576) v = ui.View() # 1000000 items vs = VirtualScroll(frame , 270, 675, 2, 1000000) v.add_subview(vs) v.present('sheet')
-
Omg @omz. I feel like I am a second grade student again with a shit load of homework ;) I don't follow your code exactly yet, but I will put in the time to understand it. Your code is very fast and impressive. Thank you to take the time.
I know this is drawing swatches. My challenge is to get it working with loading images from disk that also need to be converted to ui.Images. As I write here, I see one stupid oversight. I could just also convert the images I have downloaded from the web into ui.Images. I assume that is possible. I am sure the memory and time would be cut down significantly if I didn't have to convert JPEG to ui.Image realtime.Look however, I don't understand your code yet(I will work on it until I do),but in this day and age we need containers that are virtual(data is huge these days) I am sure you know it already. This grid for example if implemented nicely could be a list view and many other things. Could be an alternative to a tableview with custom data_source and delegate.
But you are a doer, I am a dreamer and a has been, I am Chasing you :) you have some time, LOL
-
Yeah, the first thing that starts my brain twisting is
class GridView (ui.View): def __init__(self, *args, **kwargs): ui.View.__init__(self, *args, **kwargs)
I haven't seen this before, maybe everyone knows you can do this. I have seen calling inherited classes with super... Before. Never mind I will work on it.....
The more we learn the less we know!!!
-
Going though the code @omz. Slowly, but surely. But for a beginner like me, your code below is a keeper.
# code from @omz import ui class test(ui.View): def __init__(self, frame, item_size): self.frame = frame self.item_size = item_size def frame_for_item(self, item_index): w, h = self.bounds[2:] items_per_row = int(w / self.item_size[0]) row = item_index / items_per_row col = item_index % items_per_row x_spacing = (w - (items_per_row * self.item_size[0])) / (items_per_row-1) return (col*(self.item_size[0] + x_spacing), row*self.item_size[1], self.item_size[0], self.item_size[1]) if __name__ == '__main__': frame = (0,0,540,576) item_size = (120,120) x = test(frame, item_size) for i in range(0,101): print x.frame_for_item(i)
-
Ok, my last post should have been something like -
# code from @omz import ui def frame_for_item(frame, item_size, item_index): w, h = frame[2:] items_per_row = int(w / item_size[0]) row = item_index / items_per_row col = item_index % items_per_row x_spacing = (w - (items_per_row * item_size[0])) / (items_per_row-1) return (col*(item_size[0] + x_spacing), row*item_size[1], item_size[0], item_size[1]) if __name__ == '__main__': frame = (0,0,540,576 - 44) item_size = (120,120) for i in range(0,101): print frame_for_item(frame, item_size , i )
-
I have not had much time to work on this today. But I am trying. Butchering @omz's code :) that's what you get when you pass it over to a beginner. I am making some progress at least in my eyes. I am still trying. Big birthday party to attend tonight, so have to stop working on it now :(
Regardless, was more than a start @omz. Almost all the required code is there. I could almost use it as it is for my purpose now. But want to keep working on it . I am sure someone else will come out with a wiz bang version, but it's ok. Learning a lot , and it's very useful.
My code is not trying to be tight as possible. Just getting more of the outline of how it can work with any custom ui class as a cell.
#@omz code import ui import console import math import Image _cells_created =[] _cells_deleted = [] # do color example if true, otherwise use # PosterCell as the cell. _DO_COLOR = True # the cell size used for both examples _cell_size = (64 * 2, 64 * .8) # if 0, is calculated, > 0 overrides the calc # a bug i need to fix in func frame_for_item, divide by zero error if _cells_per_row == 1. # i probably introduced the bug :( _cells_per_row = 3 # the number of cells to create for the poster demo # can be a big number, just dont want to crash # anyones device. _num_data_items_postercell_demo = 2000 class PosterCell(ui.View): def __init__(self, item_size , item_index = -1): self.frame = (0,0,item_size[0], item_size[1]) self.iv = ui.ImageView() self.iv.frame = self.frame self.add_subview(self.iv) self.item_index = item_index self.index_label = ui.Label(frame = (0,0, self.width, 14)) self.index_label.text_color = 'orchid' self.index_label.font = ('<system-bold>',18) self.add_subview(self.index_label) self.btn = ui.Button(frame = self.frame) self.add_subview(self.btn) #for debugging _cells_created.append(item_index) # i assume its smarter to wait for the call # to configure cell before loading an image def load_data(self, gridview, cell, item): if not self.iv.image: self.iv.image = ui.Image.named('ionicons-checkmark-circled-32') self.index_label.text = str(self.item_index) def cell_action_callback(self, func): self.btn.action = func def __del__(self): #for debugging _cells_deleted.append(self.item_index) class GridView (ui.View): def __init__(self, *args, **kwargs): ui.View.__init__(self, *args, **kwargs) self.visible_range = [] self.visible_views = {} self.items = [] self.reusable_cells = [] self.item_size = 0 # (120, 120) self.scrollview = ui.ScrollView(frame=self.bounds, flex='WH') self.scrollview.content_size = (0, 2000) self.scrollview.delegate = self self.data_source = None self.add_subview(self.scrollview) self.cells_per_row = 0 def reload(self): self.visible_range = [] for v in self.visible_views.values(): self.scrollview.remove_subview(v) self.visible_views = {} w, h = self.bounds[2:] #items_per_row = int(w / self.item_size[0]) items_per_row = self.xcells_per_row() num_rows = math.ceil(len(self.items) / float(items_per_row)) self.scrollview.content_size = (0, num_rows *self.item_size[1]) self.scrollview_did_scroll(self.scrollview) print 'reload' def xcells_per_row(self): if self.cells_per_row: return self.cells_per_row w, h = self.bounds[2:] return int(w / self.item_size[0]) def layout(self): self.reload() def frame_for_item(self, item_index): w, h = self.bounds[2:] #items_per_row = int(w / self.item_size[0]) items_per_row = self.xcells_per_row() row = item_index / items_per_row col = item_index % items_per_row x_spacing = (w - (items_per_row * self.item_size[0])) / (items_per_row-1) return (col*(self.item_size[0] + x_spacing), row*self.item_size[1], self.item_size[0], self.item_size[1]) def create_or_reuse_cell(self, item_index): if self.reusable_cells: cell = self.reusable_cells[0] del self.reusable_cells[0] return cell if self.data_source: return self.data_source.gridview_create_cell(self, item_index) else: return ui.View(bg_color='gray') def configure_cell(self, cell_view, item): if self.data_source: self.data_source.gridview_configure_cell(self, cell_view, item) def scrollview_did_scroll(self, scrollview): y = scrollview.content_offset[1] w, h = self.bounds[2:] items_per_row = self.xcells_per_row() #items_per_row = int(w / self.item_size[0]) first_visible_row = max(0, int(y / self.item_size[1])) num_visible_rows = int(h / self.item_size[1]) + 2 range_start = first_visible_row * items_per_row range_end = min(len(self.items), range_start + num_visible_rows * items_per_row) visible_range = range(range_start, range_end) if visible_range != self.visible_range: self.visible_range = visible_range # Remove views that are no longer visible: for i in self.visible_views.keys(): if i not in visible_range: cell = self.visible_views[i] self.reusable_cells.append(cell) self.scrollview.remove_subview(cell) # added this call and check. i think # its correct. if i didnt do it, the # __del__ method in PosterCell class # not being called if hasattr(self.visible_views[i], '__del__'): self.visible_views[i].__del__() del self.visible_views[i] # Add views that are not visible yet: for i in visible_range: if i not in self.visible_views: cell_frame = self.frame_for_item(i) # i am doing this wrong. i is not the # index item being created. need to # fugure out the real index number view = self.create_or_reuse_cell(i) view.frame = cell_frame self.configure_cell(view, self.items[i]) self.scrollview.add_subview(view) self.visible_views[i] = view def cell_callback(self, sender): #sure, my thought needed here print 'cell called us back' # not working ?? def __del__(self): print 'exiting' class GridViewDemoController (object): def __init__(self): if _DO_COLOR: from random import randint # Generate a large number of random colors: colors = ['#%02x%02x%02x' % (randint(0, 255), randint(0, 255), randint(0, 255)) for i in xrange(2000)] self.gridview = GridView( frame=(0, 0, 500, 500), background_color='white', name='GridView Demo Color') #self.gridview.item_size = (100, 120) self.gridview.item_size = _cell_size self.gridview.data_source = self self.gridview.items = colors self.gridview.cells_per_row = _cells_per_row else: self.gridview = GridView( frame=(0, 0, 500, 500), background_color='white', name='GridView Demo Poster') self.gridview.cells_per_row = _cells_per_row self.gridview.item_size = _cell_size self.gridview.data_source = self self.gridview.items = range(_num_data_items_postercell_demo) # Data source methods: def gridview_create_cell(self, gridview, item_index): # This is called when a new cell is needed. # When the grid view is scrolled, cells that become invisible are reused, # so this doesn't get called too often. if _DO_COLOR: cell = ui.View(frame=(0, 0, 100, 100)) swatch = ui.View(frame=(10, 10, 80, 60), name='swatch', flex='wh') swatch.corner_radius = 4 cell.add_subview(swatch) label = ui.Label(frame=(10, 80, 80, 15), name='label', flex='wt') label.font = ('<System>', 13) label.alignment = ui.ALIGN_CENTER label.text_color = '#333333' cell.add_subview(label) #added a btn btn = ui.Button(frame = cell.frame, name = 'btn') cell.add_subview(btn) return cell else: return PosterCell(_cell_size, item_index) def gridview_configure_cell(self, gridview, cell, item): # Note: The cell may be a new one (created by gridview_create_cell), # or an existing cell that is reused after it was scrolled out of the visible area. # This method should configure the cell to display the given item (which can be any kind of object; in this demo, all items are strings). if _DO_COLOR: cell['label'].text = item cell['swatch'].background_color = item cell['btn'].action = gridview.cell_callback else: # load a picture or whatever needs to happen cell.load_data(gridview, cell, item) # set a callback function in the cell to callback the gridview cell.cell_action_callback(gridview.cell_callback) demo = GridViewDemoController() demo.gridview.present('sheet')
-
I have tried again with this code. I have attempted to update the buffering system. I am not sure, maybe what I have done is stupid or an ok attempt. Again to be honest, it's a little beyond me. Any ideas welcomed. I don't mean you @omz, I figure you have your enough on your plate with 1.6 to worry about. I appreciate the start you gave me.
Hopefully, the new InfoCell I am using gives a better indication on how the buffering is working.
#@omz code, and mangled by @Phuket2, sorry! import ui import math _num_infocells = 2000 # cap the number of items that _reusable_cells list holds _reusable_cells_cap = 30 # the cell size _cell_size = (135, 96) # if 0, is calculated, > 0 overrides the calc # a bug i need to fix in func frame_for_item, divide by zero error if _cells_per_row == 1. # i probably introduced the bug :( _cells_per_row = 0 class InfoCell(ui.View): def __init__(self, item_size , item_index): self.frame = (0,0,item_size[0], item_size[1]) self.index = item_index index_lb = ui.Label(name = 'index_lb', frame = (0,0,self.width, self.height *.75)) index_lb.font = ('<system-bold>', 36) index_lb.alignment = ui.ALIGN_CENTER index_lb.border_width = .5 self.add_subview(index_lb) info_lb = ui.Label(name = 'info_lb', frame = (0,index_lb.height, self.width, self.height - index_lb.height )) index_lb.font = ('<system-bold>', 22) info_lb.alignment = ui.ALIGN_CENTER info_lb.background_color = 'orchid' self.add_subview(info_lb) btn = ui.Button(name = 'btn', frame = self.frame) btn.action = self.cell_action_callback self.add_subview(btn) def load_data(self, gridview, cell, item): self['index_lb'].text = str(item) def set_info_label(self, text): self['info_lb'].text = text if text == 'Created': self['info_lb'].background_color = 'red' else: self['info_lb'].background_color = 'orange' def cell_action_callback(self, func): self['btn'].action = func def __del__(self): pass class GridView (ui.View): def __init__(self, item_size, cells_per_row = None , *args, **kwargs): ui.View.__init__(self, *args, **kwargs) self.visible_range = [] self.visible_views = {} self.items = [] self.reusable_cells = [] self.item_size = item_size self.item_h , self.item_w = item_size self.scrollview = ui.ScrollView(frame=self.bounds, flex='WH') self.scrollview.content_size = (0, 2000) self.scrollview.delegate = self self.data_source = None self.add_subview(self.scrollview) #used to aviod redundant calls from reload function. self.screen_size = (0,0) def reload(self): # aviod double call at launch. overhead # can be small, but if loading pics etc. # better to aviod it. if self.screen_size == ui.get_screen_size() : return self.visible_range = [] for v in self.visible_views.values(): self.scrollview.remove_subview(v) self.visible_views = {} w, h = self.bounds[2:] #items_per_row = int(w / self.item_size[0]) items_per_row = self.xcells_per_row() num_rows = math.ceil(len(self.items) / float(items_per_row)) self.scrollview.content_size = (0, num_rows *self.item_size[1]) self.scrollview_did_scroll(self.scrollview) self.screen_size = ui.get_screen_size() #self.cell_buffer[0] self.num_rows = num_rows print 'reload' def xcells_per_row(self): if self.cells_per_row: return self.cells_per_row w, h = self.bounds[2:] return int(w / self.item_size[0]) def layout(self): self.reload() def frame_for_item(self, item_index): w, h = self.bounds[2:] #items_per_row = int(w / self.item_size[0]) items_per_row = self.xcells_per_row() row = item_index / items_per_row col = item_index % items_per_row x_spacing = (w - (items_per_row * self.item_size[0])) / (items_per_row-1) return (col*(self.item_size[0] + x_spacing), row*self.item_size[1], self.item_size[0], self.item_size[1]) def create_or_reuse_cell(self, item_index): # @Phuket2 # trying to change the buffering system # assuming that resuable_cells is # calculated well # using a sledge hammer here! i can see when # scrolling around fast some intresting results. not sure its good ir right through # maybe rather than deleting the first entries in the list, # i could iterate over the list # and delete elements that are furthest away # from the currently visible row, fwd and backwards. just a thought. # its very difficult for me to predict the correct approach. # size of buffer, speed of alogorithm to # etc... # cap the buffered items buf_size = _reusable_cells_cap # hmmm, i am not really sure all the clean # up of objects are being handled correctly # here num_buf_items = len(self.reusable_cells) if num_buf_items > buf_size: del self.reusable_cells[:num_buf_items -buf_size] self.name = str (len(self.reusable_cells)) for cell in self.reusable_cells: if cell.index == item_index: cell.set_info_label('reused') return cell # @Phuket2 # i commented out the below code ''' if self.reusable_cells: cell = self.reusable_cells[0] del self.reusable_cells[0] return cell ''' if self.data_source: return self.data_source.gridview_create_cell(self, item_index) else: return ui.View(bg_color='gray') def configure_cell(self, cell_view, item): if self.data_source: self.data_source.gridview_configure_cell(self, cell_view, item) def scrollview_did_scroll(self, scrollview): y = scrollview.content_offset[1] w, h = self.bounds[2:] items_per_row = self.xcells_per_row() #items_per_row = int(w / self.item_size[0]) first_visible_row = max(0, int(y / self.item_size[1])) # @Phuket2 # pretty sure the + 2 below is the 'read ahead' buffer of rows. # will play with this value later num_visible_rows = int(h / self.item_size[1]) + 2 range_start = first_visible_row * items_per_row range_end = min(len(self.items), range_start + num_visible_rows * items_per_row) visible_range = range(range_start, range_end) if visible_range != self.visible_range: self.visible_range = visible_range # Remove views that are no longer visible: for i in self.visible_views.keys(): if i not in visible_range: cell = self.visible_views[i] self.reusable_cells.append(cell) self.scrollview.remove_subview(cell) # @Phuket2 # added this call and check. i think # its correct. if i didnt do it, the # __del__ method in PosterCell class # not being called if hasattr(self.visible_views[i], '__del__'): self.visible_views[i].__del__() del self.visible_views[i] # Add views that are not visible yet: for i in visible_range: if i not in self.visible_views: cell_frame = self.frame_for_item(i) view = self.create_or_reuse_cell(i) view.frame = cell_frame self.configure_cell(view, self.items[i]) self.scrollview.add_subview(view) self.visible_views[i] = view def cell_callback(self, sender): # @Phuket2 #sure, more thought needed here print 'cell called us back' # @Phuket2 # not sure why its not working ?? # seems to never be called def __del__(self): print 'exiting' class GridViewDemoController (object): def __init__(self): self.gridview = GridView( _cell_size,_cells_per_row, frame=(0, 0, 500, 500), background_color='white', name='GridView Demo Poster') self.gridview.cells_per_row = _cells_per_row self.gridview.item_size = _cell_size self.gridview.data_source = self self.gridview.items =range( _num_infocells) # Data source methods: def gridview_create_cell(self, gridview, item_index): # This is called when a new cell is needed. # When the grid view is scrolled, cells that become invisible are reused, # so this doesn't get called too often. cell = InfoCell(_cell_size, item_index ) cell.set_info_label('Created') return cell def gridview_configure_cell(self, gridview, cell, item): # Note: The cell may be a new one (created by gridview_create_cell), # or an existing cell that is reused after it was scrolled out of the visible area. # This method should configure the cell to display the given item (which can be any kind of object; in this demo, all items are strings). cell.load_data(gridview, cell, item) cell.cell_action_callback(gridview.cell_callback) _hide_tb = False demo = GridViewDemoController() demo.gridview.present('sheet', hide_title_bar = _hide_tb )