Skip to content

Instantly share code, notes, and snippets.

/vpv.py

Created Aug 17, 2015
Embed
What would you like to do?
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
'''
17 Aug 2015, color_demo, added random color when clicked.
'''
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 = 'color_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()
# create a button to get a click
btn = ui.Button(frame = self.frame)
btn.action = self.color_cell_hit
self.add_subview(btn)
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)
def color_cell_hit(self, sender):
self.background_color = random_color()
@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