vpv.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coding: utf-8 | |
# Pythonista 1.6 20:14 | |
# many things just copied from omz starter code | |
# all the bad stuff, thats me :) | |
# still learning | |
###### VirtualView ####### | |
# for displaying large amounts of data | |
''' | |
The link to this code | |
https://gist.github.com/Phuket2/3a32cc584abc3005b29f | |
17 Aug 2015 | |
1. color_demo, added random color when clicked. | |
18 Aug 2015 | |
1. using gistcheck.py to commit this file to gisthub. | |
i thought it was working, its not working though. | |
i want to punch it, drives me crazy. | |
2. **** Important ***** | |
no need to add a button for a hit test now. when you create your cell, you can set self.action to a method/function. if the cell is clicked/hit, the self.action will be called if defined. | |
Changed all the demo cells to work off the new mechanism. | |
3. changing the click mechanism had a side effect. the cell was not being released from memory when setting self.action = some_method. In the release | |
function i now set self.action = None, the memory leak stopped. | |
4. added 'Top' and 'Bottom' menu items to the titlebar. just to take you to the top abd bottom of the list. | |
5. the initial cell count is 100 million. its a VirtualView. 5 or 500 million cells should not matter. if you are not comportable with that number, just change _item_count below to a smaller number! | |
6. changed the ui.Label width to be able to display the number correcly. did it the lazy way :( | |
''' | |
import ui | |
from random import randint | |
import time | |
import console | |
# determining if Phytonista 1.5 or 1.6xxx | |
__ver__ = 0 | |
try: | |
import dialogs | |
__ver__ = 1.6 | |
except ImportError: | |
__ver__ = 1.5 | |
# some pythonista built in image names for demo | |
images = ['Confounded', 'Crying_2', 'Disappointed' , 'Dizzy', 'Flushed', 'Smiling_3', 'Winking', 'Tired', 'Unamused', 'Fisted_Hand', 'Index_Finger_Up_1', 'Four_Leaf_Clover', 'Herb', 'Mushroom', 'Baby_Chick_1', 'Bactrian_Camel', 'Boar', 'Cat_Face_Grinning','Cat_Face_Heart-Shaped_Eyes', 'Cow_Face', 'Honeybee'] | |
def random_color(): | |
# for demo purposes | |
return(randint(0,255) / 255., randint(0,255) / 255., randint(0,255) / 255.) | |
def invert_rgb(color): | |
return(255 - color[0], 255 - color[1], 255 - color[2], 0) | |
def rand_image(): | |
# just return an image from, a list (subset) | |
# of pythonista built images. | |
# testing/demo purposes | |
return ui.Image.named(images[randint(0,len(images) -1)]) | |
# for demo purposes, can use image_demo, color_demo, row_demo | |
_cell_type = 'row_demo' | |
# for demo purposes, can set the item_size here | |
# item_size is width, height. if either w,h = 0 | |
# then the width or height or both will be the | |
# value of the presented view | |
_item_size = (128, 128) | |
# just to make demo life a little easier. | |
# if using 'row_type' as cell, sets the width to 0 | |
if _cell_type == 'row_demo': | |
_item_size = (0, _item_size[1]) | |
# can provide data in 2 ways when creating the | |
# VirtualView. You can pass item_count, no data. | |
# or you can pass a list of whatever. | |
# passing item_count negates having to do | |
# something like range(100000) to pass in. | |
_item_count = (1000000 * 100) + 1 # 100 million and one cells | |
#should be ok. What is nice, is ui.ScrollView is | |
# clean in terms of memory usage. not to imply that | |
# it would be wrong. just nice it is not. | |
_buf_size = 80 | |
# presentation style | |
_pres_style = 'sheet' | |
class VirtualCell(ui.View): | |
def __init__(self, caller, item_index, data_item, w, h, cell_type = None): | |
""" | |
A cell object to be used by the VirtualView | |
Class. | |
Attributes: | |
caller: | |
the instanciating object | |
item_index: | |
the items index as displayed by the Virtual | |
data_item: | |
the data to be drawn for the cell. can be None if the VirtualView is not using data | |
w: | |
the width of the cell being requested | |
h: | |
the height of the cell being requested | |
cell_type: | |
the idea is that the ui elements for a cell are built in their own method. Making it easier to create different cell types. | |
i guess this could have been done a few ways, inheritance or just a copy abd paste to make a new cell. | |
As i get better, i hope to improve the design, but this is what i have for now. | |
if cell_type is None, will call create_cell_contents method from init. otherwise you could create your own methods and call them appropriately. | |
i try to give an example. | |
""" | |
self.caller = caller | |
self.item_index = item_index | |
self.width, self.height = w, h | |
self.action = None | |
# for row_demo cell_type | |
self.selected = False | |
# just for demo/testing purposes | |
# would normally just call create_cell_contents | |
if not cell_type: | |
self.create_cell_contents() | |
elif cell_type == 'image_demo': | |
self.create_image_cell_contents() | |
elif cell_type == 'color_demo': | |
self.create_color_cell_contents() | |
elif cell_type == 'row_demo': | |
self.create_row_cell_contents() | |
else: | |
# if we dont recognise the cell_type | |
# call the default | |
self.create_cell_contents() | |
if cell_type <> 'row_demo': | |
# adds a label to the cell with the | |
# item_index in the top right corner. | |
# for debugging only | |
self.add_item_id_label() | |
def create_cell_contents(self): | |
# the place you would typically create your | |
# cells ui elements | |
pass | |
def create_color_cell_contents(self): | |
# demo of using random color swatchs | |
self.background_color = random_color() | |
self.action = self.set_bgcolor_to_random | |
def create_image_cell_contents(self): | |
# demo of displaying images. Just changes | |
# the image when the cell is clicked | |
img = ui.ImageView(name = 'img', frame = self.frame) | |
img.image = rand_image() | |
self.add_subview(img) | |
self.action = self.set_random_image | |
def create_row_cell_contents(self): | |
# demo of virtual rows. is not smart as the | |
# ui.TableView does this a billion times better | |
self.flex = 'W' | |
self.ignore_width = True | |
lb = ui.Label(frame = self.frame) | |
lb.text = '{:,}'.format(self.item_index) | |
lb.alignment = ui.ALIGN_LEFT | |
lb.font = ('Menlo', 24) | |
lb.x += 30 | |
lb.width -= 30 | |
lb.center = self.center | |
self.border_width = .5 | |
if not self.item_index % 2: | |
self.background_color = 'lightyellow' | |
else: | |
self.background_color = 'lightgray' | |
self.add_subview(lb) | |
self.action = self.cell_row_click | |
# action functions for when we are clicked, cell | |
# type to define. otherwise None | |
def set_bgcolor_to_random(self): | |
self.background_color = random_color() | |
def set_random_image(self): | |
iv = self['img'] | |
iv.image = None | |
iv.image = rand_image() | |
@ui.in_background | |
def cell_row_click(self): | |
# called when clicking on a row, with row_demo cell_type | |
# my stupid attempt to try to simulate a row click. | |
# i have tried a few things here. seems like | |
# over kill. the current way, just using invert | |
# rgb. not a great method. but illustrates what | |
# can be done for now | |
if self.selected : return | |
self.selected = True | |
old_color = self.background_color | |
self.background_color = invert_rgb(self.background_color) | |
time.sleep(.12) | |
self.background_color = old_color | |
self.selected = False | |
def add_item_id_label(self): | |
# just add a label to see the item_index. | |
# handy for debugging | |
txt = '{:,}'.format(self.item_index) | |
w, h = 10 * len(txt), 20 | |
f = (self.width - w, 0, w , h) | |
lb = ui.Label( frame = f) | |
lb.frame = f | |
lb.background_color = 'black' | |
lb.text_color = 'white' | |
lb.text = txt | |
lb.alignment = ui.ALIGN_RIGHT | |
self.add_subview(lb) | |
lb.bring_to_front() | |
def layout(self): | |
pass | |
# no need to add a btn to the view for a click | |
# event. this works very well. | |
# As long as we are ok with the whole cell | |
# ignoring, touch_began, touch_moved. Of course, | |
# other opporturnities to use these events. | |
# KISS at the moment :) | |
def touch_ended(self, touch): | |
if self.action: | |
self.action() | |
# explicity call this method, to free up | |
# resources. whatever they maybe. dont rely on | |
# __del__ | |
def release(self): | |
# remove ourself from the superview | |
# maybe this is a stupid idea. Maybe | |
# smarter for the caller to remove us? | |
self.superview.remove_subview(self) | |
# this would not always be required. but | |
# i found when a button for example has a | |
# action attached, even internal to this | |
# class. Memory is not being released unless | |
# the subviews are removed. thats only one | |
# example. could be others, if using other | |
# callbacks such as delegates. | |
for sv in self.subviews: | |
self.remove_subview(sv) | |
# if i dont do this, the cell is not resleased | |
# from memory! | |
self.action = None | |
def __del__(self): | |
# This does get called. but from what i read, | |
# better to have a explicit function, rather | |
# than rely on __del__. i am not good enough | |
# to understand it. So for now, i call the | |
# classes release method explicitly. | |
# Seems a shame. Would make cleaning up so | |
# much more natural. In my view. | |
#print '__del__ was called on item {}'.format(self.item_index) | |
pass | |
class VirtualView(ui.View): | |
def __init__(self, vw, vh, item_size, buf_size = 0, items= None, item_count = 0): | |
""" | |
A VirtualView for displaying large | |
amounts of data in scrollable area. | |
Attributes: | |
item_width: | |
width of cell | |
item_height: | |
heigth of cell | |
buf_size: | |
items: | |
item_count: | |
""" | |
# set the width and height of the view | |
self.width = vw | |
self.height = vh | |
# buf_size gets set in the layout method. | |
# currently if its smaller than one screen | |
# of items, its resized to be able to hold | |
# at least one screen of items | |
self.buf_size = buf_size | |
# a list of VirtualCell objects, the number of | |
# buffered Cells, dedends on bufsize | |
self.buffer = [] | |
# calculated in layout method | |
self.visible_rows = 0 | |
self.items_per_row = 0 | |
# store the the w,h like this for ease of reading | |
self.item_width = item_size[0] | |
self.item_height = item_size[1] | |
# flags to indicate that the width of height or | |
# both should be overwridden. if item_width or | |
# item height is 0, the width or height is used | |
self.override_width = False | |
self.override_height = False | |
if self.item_width == 0: | |
self.override_width = True | |
if self.item_height == 0: | |
self.override_height = True | |
# to make sure we are called by layout | |
self.flex = 'WH' | |
# set up a scrollview, with delagate | |
self.sv = ui.ScrollView(flex='WH') | |
self.sv.frame = self.frame | |
self.sv.delegate = self | |
self.add_subview(self.sv) | |
# a list of data to be used | |
self.items = items | |
# num_items is only set to non zero value if | |
# you dont require data. | |
# eg. you could pass 1,000,000 to item_count | |
# to have a virtual view of a 1million items. | |
# of course you could use a range() for items | |
# but for large numbers, is not efficent. | |
self.num_items = item_count | |
# just demo/debug menu title btns | |
btn = ui.ButtonItem(title = 'Top') | |
btn.action = self.goto_top | |
# spacer button :( | |
btn1 = ui.ButtonItem(title =' ' * 5) | |
btn2 = ui.ButtonItem(title ='Bottom') | |
btn2.action = self.goto_end | |
self.right_button_items = [btn2, btn1, btn] | |
# some debug vars | |
self.created_cells = 0 | |
self.deleted_cells = 0 | |
self.used_buffered_cells = 0 | |
def goto_top(self, sender): | |
self.sv.content_offset = (0,0) | |
def goto_end(self, sender): | |
self.sv.content_offset = (0, self.sv.content_size[1] - self.height) | |
# this method returns the data item count | |
def item_count(self): | |
# we dont access len(self.items) in the code, | |
# we call this method. maybe no data items, | |
# just a int for the count. | |
return self.num_items if self.num_items > 0 else len(self.items) | |
# ui.View callback | |
def layout(self): | |
w,h = self.bounds[2:] | |
# see if we are overriding the item_size | |
if self.override_width and self.override_height: | |
self.item_width , self.item_heigth = self.bounds[2:] | |
elif self.override_width: | |
self.item_width = self.width | |
elif self.override_height: | |
self.item_height = self.height | |
# number of visible rows | |
self.visible_rows = int(h / self.item_height) | |
# adding 2 addtional visible rows here. nicer | |
# scrolling. extra line and comment, just so | |
# its not just a magic + 2 | |
self.visible_rows += 2 | |
# items per row | |
self.items_per_row =int(w / self.item_width) | |
# maximum num of rows | |
max_rows = (self.item_count() / self.items_per_row) | |
# add an extra row if not an exact multiple | |
if self.item_count() % self.items_per_row <> 0: | |
max_rows += 1 | |
# set the content height of the scrollview | |
self.sv.content_size =(0, max_rows * self.item_height) | |
# the way the buffer is implemented it does | |
# not work correctly if it is smaller than | |
# the number of items on the screen. | |
# can be done, more a performance issue | |
# so the buffer is resized here if required. | |
min_buf_size = self.visible_rows * self.items_per_row | |
if self.buf_size < min_buf_size: | |
self.buf_size = min_buf_size | |
# clear the buffer, remove all the subviews | |
# needed when the orientation changes, is not | |
# called on all presentation styles. Not an | |
# issue, layout is called by ui when req. | |
for cell in self.buffer: | |
cell.release() | |
del cell | |
self.deleted_cells += 1 # debug | |
self.buffer = [] | |
#self.screen_size = ui.get_screen_size() | |
# ui.View callback | |
def draw(self): | |
# useful to use the draw method of ui | |
# on startup and orientation change we | |
# are called as expected. | |
# this method is also called explictly from | |
# scrollview_did_scroll | |
v_offset = self.sv.content_offset[1] | |
# get the first visible row | |
row = int(v_offset / self.item_height) | |
# get the visible items to draw | |
visible_items = self.items_visible() | |
# draw each item | |
for item_index in visible_items: | |
self.draw_item(item_index) | |
# ui callback, ui.scrollview delegate | |
def scrollview_did_scroll(self, scrollview): | |
self.draw() | |
def draw_item(self, item_index): | |
# if the item_index is found in the buffer | |
# we know the cell is still present in the | |
# scrollview. so we exit, we dont need to | |
# recreate the cell | |
for cell in self.buffer: | |
if cell.item_index == item_index: | |
self.used_buffered_cells += 1 # debug | |
return | |
# create a cell (view), that is added to the | |
# subviews of the scrollview. once the buffer | |
# is full, the oldest cell is removed from | |
# the scrollview.subviews | |
data_item = None | |
if self.num_items == 0: | |
data_item = self.items[item_index] | |
cell = VirtualCell(self, item_index, data_item, self.item_width, self.item_height, cell_type = _cell_type) | |
self.created_cells += 1 | |
self.sv.add_subview(cell) | |
cell.frame = self.frame_for_item(item_index) | |
# maintain the buffer | |
self.buffer.append(cell) | |
if len(self.buffer) > self.buf_size: | |
cell = self.buffer[0] | |
cell.release() | |
del cell | |
self.buffer.pop(0) | |
self.deleted_cells += 1 # debug | |
# get the frame for the given item index | |
def frame_for_item(self, item_index): | |
w, h = self.bounds[2:] | |
items_per_row = self.items_per_row | |
row = item_index / items_per_row | |
col = item_index % items_per_row | |
if items_per_row == 1: | |
x_spacing = (w - (items_per_row * self.item_width)) / (items_per_row) | |
else: | |
x_spacing = (w - (items_per_row * self.item_width)) / (items_per_row-1) | |
return (col*(self.item_width + x_spacing), row*self.item_height, self.item_width, self.item_height) | |
# get the visible indices as a range... | |
def items_visible(self): | |
y = self.sv.content_offset[1] | |
w, h = self.bounds[2:] | |
items_per_row = self.items_per_row | |
num_visible_rows = self.visible_rows | |
first_visible_row = max(0, int(y / self.item_height)) | |
range_start = first_visible_row * items_per_row | |
range_end = min(self.item_count(), range_start + num_visible_rows * items_per_row) | |
return range(range_start, range_end) | |
# ui.View callback | |
def will_close(self): | |
# do some clean up, make sure nothing | |
# left behind in memory. | |
# maybe this is not need, but easy to do it | |
for cell in self.buffer: | |
cell.release() | |
del cell | |
self.deleted_cells += 1 | |
self.buffer = [] | |
# print out some debug info | |
print 'Created Cells = {}, Deleted Cells = {}, Used buffered Cells = {}'.format(self.created_cells, self.deleted_cells, self.used_buffered_cells) | |
if __name__ == '__main__': | |
if __ver__ == 1.6: | |
console.set_font('Menlo', 22) | |
# as our item size is 128,128 defined at the top | |
# gives us a window size to view 4 rows x 4 cols | |
vw = 128 * 4 | |
vh = 128 * 4 | |
else: | |
vw , vh = 540, 576 | |
vv = VirtualView(vw, vh, _item_size, _buf_size, items = None , item_count = _item_count) | |
vv.present(_pres_style, hide_title_bar = False ) |
cclauss
commented
Aug 18, 2015
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment