Skip to content

Instantly share code, notes, and snippets.

/vpv.py

Created Aug 18, 2015
Embed
What would you like to do?
vpv.py
# 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

This comment has been minimized.

Copy link

@cclauss cclauss commented Aug 18, 2015

__pythonista_ver__ = 0
try:
    import dialogs
    __pythonista_ver__ = 1.6
except ImportError:
    __pythonista_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'''.split()

def random_color():
    # for demo purposes
    return(random.random(), random.random(), random.random())

def invert_rgb(color):
    return(255 - color[0], 255 - color[1], 255 - color[2], 0)

def random_image():
    # just return an image from, a list (subset)
    # of pythonista built images.
    # testing/demo purposes
    return ui.Image.named(random.choice(images))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.