And now, Lists – the motion picture:
http://www.youtube.com/watch?v=Kv6OC8c4h7Q
I deleted the first draft code so I could slide the video in here. The code a few messages down matches the functionality shown here.
And now, Lists – the motion picture:
http://www.youtube.com/watch?v=Kv6OC8c4h7Q
I deleted the first draft code so I could slide the video in here. The code a few messages down matches the functionality shown here.
I love experimental UI stuff and am excited to try this, but it seems that it is relying on some project data that is not there —
Line 75: s = readProjectData("lists") -- returns nil
This causes an error on line 76 with string.gmatch.
Could amend your example to create some project data first?
(We really need to come up with a solution for sharing project data.)
Blast, let me fix it. That’s just the line that retrieves any existing list.
There. See if that did it.
Very interesting! The big letters remind me a bit of the Metro UI that Microsoft is showing.
One criticism I have is that I confuse the swipe left and swipe right gestures.
Gestures are good, but I think what will help cement them is to tie them into animations. For example:
Thanks for sharing the cool project. I love UI ideas like this.
Edit: It also reminds me a bit of Clear for iPhone: http://www.realmacsoftware.com/clear/
Thanks, Simeon. Yeah, I knew I was implementing the delete “backwards,” but somehow in my head it just seemed wrong to put subitems left of their parent.
I think you’re right that tying in some animation would go a long way (though I want to avoid having any kind of button or marker appear if I can avoid it).
What do you think about having marked items fade out and self-delete after a few seconds?
Off to fiddle.
Fair enough about the button or marker. I agree it goes against the concept. Would you say that the Clear UI goes against this principle? I am uncertain if it does — it does show descriptive text while a gesture is in-progress, but that is just text.
Having marked items remove themselves seems like a good idea. As long as you can “catch” them if you change your mind (before they are completely removed).
I think the key is to know what a gesture is going to do before you have completed the gesture, and always be able to back out of the gesture. (This doesn’t apply to tap and long-press gestures).
Give this version a shot. Still a couple of behaviors to neaten up, but I think it illustrates all the ideas. Sweep left and right to navigate, touch to mark. Marked items fade and disappear after a few moments, but can be “saved” it you tap again before they are gone.
This version implements visible push for left and right and uses the fade away idea for deletion. That gets the gestures down to five: tap to create, tap item to mark/unmark, double-tap to edit and push left or right to navigate.
Task = class()
function Task:init(name, c, parent)
self.id = math.floor(ElapsedTime * 100000)
self.parent = parent
self.name = name
self.backColor = c
self.textColor = color(31, 31, 31, 255)
self.marked = false
self.top = 0
self.bottom = 0
end
function Task:draw(x)
pushStyle()
fontSize(24)
w,h = textSize(self.name)
fill(self.backColor)
stroke(self.backColor)
rect(x, self.bottom, WIDTH, 50)
fill(self.textColor)
text(self.name, 4 + x, self.bottom + 4)
if self.marked then
stroke(145, 38, 27, self.textColor.a)
strokeWidth(8)
line(4 + x, self.bottom + 20,
4 + w + x, self.bottom + 20)
end
-- fade marked items
if self.marked then
self.textColor.a = self.textColor.a - 2
end
end
function Task:touched(touch)
if touch.y >= self.bottom and touch.y <= self.top then
return true
end
return false
end
function Task:toString()
local s, p
if self.marked then s = "1" else s = "0" end
return self.id..","..self.parent..","..self.name..","..s
end
Main for above…
-- Main
-- Lists
-- An experiment in simplicity
displayMode(FULLSCREEN)
NORMAL = 0
EDIT = 1
MARKED = 2
SWEEPRIGHT = 3
function setup()
tasks = {}
-- clearProjectData()
readLists()
basetask = Task("Lists", color(221, 149, 149, 88), "none")
basetask.id = "base"
tasks["base"] = basetask
selected = nil
selectedtask = nil
newtask = nil
timer = 0
status = NORMAL
oldstate = nil
font("HelveticaNeue")
textMode(CORNER)
textAlign(LEFT)
sweepX = 0
end
function readLists()
local k, t, s, i, id, name, parent, marked
s = readProjectData("lists")
if s ~= nil then
for k in string.gmatch(s,"([^|]+)") do
i = 0
for t in string.gmatch(k,"([^,]+)") do
i = i + 1
if i == 1 then
id = t
elseif i == 2 then
parent = t
elseif i == 3 then
name = t
elseif i == 4 then
if t == "0" then marked = false
else marked = true end
end
end
--print(id, name, parent, marked)
newtask = Task(name, color(math.random(55)+200,
math.random(55)+200, math.random(55)+200, 55),
parent)
newtask.id = id
newtask.marked = marked
table.insert(tasks, 1, newtask)
end
end
end
function saveLists()
local k, t, s
s = ""
for k,t in pairs(tasks) do
if t.id ~= "base" then
if string.len(s) > 0 then s = s.."|" end
s = s..t:toString()
end
end
saveProjectData("lists", s)
end
function keyboard(key)
if status == EDIT then
if key ~= nil then
if string.byte(key) == 10 then
if string.len(tasks[selected].name) == 0 then
table.remove(tasks, selected)
selecteditem = nil
selected = nil
end
cleanUp()
elseif string.byte(key) == nil then
if string.len(tasks[selected].name) > 0 then
tasks[selected].name =
string.sub(tasks[selected].name,
1, string.len(tasks[selected].name) - 1)
end
else
tasks[selected].name =
tasks[selected].name..key
end
end
end
end
function cleanUp()
status = NORMAL
sweepX = 0
hideKeyboard()
saveLists()
end
function taskList(base, x)
local k, t, i
pushStyle()
fontSize(244)
fill(0, 0, 0, 42)
s = base.name
w,h = textSize(s)
text(base.name, 0 + x, HEIGHT - h)
fontSize(24)
i=0
for k, t in pairs(tasks) do
if t.parent == base.id then
i = i + 1
t.bottom = HEIGHT - i * 50
t.top = t.bottom + 50
t:draw(x)
end
end
popStyle()
end
function findTask(id)
local k, t
for k,t in pairs(tasks) do
if t.id == id then return t end
end
return nil
end
function draw()
local s, w, h, k, t, ct, i, ci
-- delete faded items and their kids
for i, t in ipairs(tasks) do
if t.textColor.a < 2 then
for ci, ct in ipairs(tasks) do
if ct.parent == t.id then
table.remove(tasks, ci)
end
end
table.remove(tasks, i)
end
end
-- drawing
background(171, 171, 182, 255)
if sweepX ~= 0 then
if sweepX < 0 then
taskList(selectedtask, WIDTH + sweepX)
clip(0 + sweepX, 0, WIDTH, HEIGHT)
taskList(basetask, 0 + sweepX)
noClip()
end
if sweepX > 0 and basetask.parent ~= "none" then
taskList(basetask, 0 + sweepX)
clip(0 + sweepX - WIDTH, 0, WIDTH, HEIGHT)
taskList(findTask(basetask.parent), 0 + sweepX - WIDTH)
noClip()
end
stroke(127, 127, 127, 113)
strokeWidth(3)
line(WIDTH + sweepX, HEIGHT, WIDTH + sweepX, 0)
else
taskList(basetask, 0)
end
if status == EDIT and ElapsedTime > timer + 0.2 then
strokeWidth(3)
stroke(0, 0, 0, 255)
w,h = textSize(tasks[selected].name)
line(8 + w, tasks[selected].bottom + 10,
8 + w, tasks[selected].top - 20)
end
if ElapsedTime > timer + 0.4 then
timer = ElapsedTime
end
-- touch handling
if CurrentTouch.state == BEGAN then
sweepX = 0
-- find the item being selected
selectedtask = nil
for k, t in pairs(tasks) do
if t.parent == basetask.id and t:touched(CurrentTouch) then
selected = k
selectedtask = t
end
end
end
if CurrentTouch.state == MOVING then
sweepX = sweepX + CurrentTouch.deltaX
if sweepX < 0 and selectedtask == nil then sweepX = 0 end
if sweepX > 0 and basetask.id == "base" then sweepX = 0 end
end
if CurrentTouch.state == ENDED and
CurrentTouch.state ~= oldstate then
if selectedtask ~= nil then
if CurrentTouch.tapCount > 1 then
-- edit an item
status = EDIT
tasks[selected].marked = false
showKeyboard()
elseif sweepX < (WIDTH / 2) * - 1 then
basetask = selectedtask
selectedtask = nil
cleanUp()
elseif oldstate ~= MOVING then
-- mark an item
tasks[selected].marked = not tasks[selected].marked
if not tasks[selected].marked then
tasks[selected].textColor.a = 255
end
cleanUp()
end
else
if oldstate ~= MOVING then
-- new task
newtask = Task("", color(math.random(55)+200,
math.random(55)+200, math.random(55)+200, 55),
basetask.id)
table.insert(tasks, 1, newtask)
selected = 1
status = EDIT
showKeyboard()
elseif sweepX > (WIDTH / 2) then
-- up a level
basetask = findTask(basetask.parent)
selectedtask = nil
end
end
sweepX = 0
end
oldstate = CurrentTouch.state
end
Zero UI is interesting. It gains much interest with touch-based devices. Good zero UI has to be as intuitive as possible. User must be able to figure it out without learning, ideally. Once a user must learn how to use it, IMHO it breaks the whole concept and principle of zero UI.
So, I guess the question is – does Lists make it over that hurdle. Does it meet the goal I had at the outset: no interface, but intuitive behavior.
I really like the changes you added. The drag movement makes sublists much more intuitive.
The only really noticeable issue is that the animation snaps to the next state based on whether it’s more than 50% on or off the page. Ideally you’d like it to continue moving with the momentum of your drag gesture and settle gently into the next state.
Have you had a look at the Clear UI? They seem to be going for a similar goal. They use folding and unfolding metaphors as well.
Cool concept! However it surprised me that new items were created at the top of a list. So for me I changed it to
table.insert(tasks, newtask)
selected = #tasks
I also would change the required sweep length to a fixed value rather than WIDTH/2. This probably works well in portrait mode, but in landscape mode I found myself often sweeping short of this target.
But to answer your question, for me it makes it over the hurdle! Thanks for sharing it!
To answer Mark’s question, we have to give this app (not the code) to a independen tester i.e. someone who knows about list but never use/know this app (or something similar) before. If s/he can interact with the list without being told how to do it, then clearly the UI works very well. Our answer shouldn’t be taken into account because we -as programmers- have known and understand the code beneath the UI.
This is why I love Codea. It makes it easy to make our own utilitys too that we use in real life
P.S. It looks really nice. Dang. Give Mark an app, let him touch it, and it will be magical. My mom actually is thinking about using it in real life shopping. This is awsome
Thanks, @Zoyt. Much appreciated, and if your mom really does use it, let me know how it goes.
@Herwig, yeah I debated that myself. Something about top worked better for me. BTW, at one point there was no seperate ID in the Task class, just a generated key for each item, but no matter how often I applied table.sort, I couldn’t keep things in order. I’m sure I just slipped up on the logic somewhere, but I eventually seperated the ID and key and went back to using insert.
@Simeon and @Herwig, yeah, it would be much neater if it worked more from momentum than pure position. I think I can make that happen.
It’s not too hard to make a settings, is it?
Great concept