vpv.py
# coding: utf-8 | |
# 1.6 | |
# many things just copied from omz starter code | |
# all the bad stuff, thats me :) | |
# still learning | |
###### VirtualView ####### | |
# for displaying large amounts of data | |
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 Exception as e: | |
__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 = 'image_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) | |
# 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 = 100000 # 100,000 | |
_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 | |
# 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': | |
self.add_item_id_tr() | |
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() | |
def create_image_cell_contents(self): | |
# demo of displaying images. has a button | |
# overlayed with a action, just to change | |
# the image when clicked | |
img = ui.ImageView(name = 'img', frame = self.frame) | |
img.image = rand_image() | |
self.add_subview(img) | |
btn = ui.Button(name = 'btn', frame = self.frame) | |
btn.action = self.hit | |
self.add_subview(btn) | |
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 = str(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) | |
btn = ui.Button(frame = self.frame) | |
btn.action = self.cell_row_click | |
self.add_subview(btn) | |
@ui.in_background | |
def cell_row_click(self, sender): | |
# 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, showing and hiding | |
# a button with a reduced alpha, seems ok, but | |
# does not work. only works on the first row. | |
# not sure whats going on yet :( | |
if self.selected : return | |
self.selected = True | |
btn = ui.Button(frame = self.frame) | |
btn.background_color = 'green' | |
btn.alpha = .3 | |
self.add_subview(btn) | |
delay = .15 | |
#inverted_color = invert_rgb(self.background_color) | |
print self.item_index | |
#old_bg_color = self.background_color | |
#self.background_color = inverted_color | |
time.sleep(delay) | |
#self.background_color = old_bg_color | |
self.remove_subview(btn) | |
self.selected = False | |
def add_item_id_tr(self): | |
scale = .2 | |
w, h = self.width * scale, self.height * scale | |
f = (self.width - w, 0, w + (scale * .5), h) | |
lb = ui.Label( frame = f) | |
lb.frame = f | |
lb.background_color = 'black' | |
lb.text_color = 'white' | |
lb.text = str(self.item_index) | |
lb.alignment = ui.ALIGN_CENTER | |
self.add_subview(lb) | |
lb.bring_to_front() | |
def layout(self): | |
pass | |
# 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) | |
def hit(self, sender): | |
self['img'].image = None | |
self['img'].image = rand_image() | |
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 | |
# some debug vars | |
self.created_cells = 0 | |
self.deleted_cells = 0 | |
self.used_buffered_cells = 0 | |
# 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 ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.