六、點擊檢定,慣性,加速度,彈簧及動畫效果6.1,點擊檢定
由於沒有使用控制項,那如何判定手指(滑鼠)點在哪個元素上面?——這個得自己做這個判斷。
我給每個介面元素設定了一個點擊檢定框,這個框其實是一個矩形,要判斷一個點是否在一個矩形之內是很容易的,實現起來是不是應該很簡單?實際上比你想的要稍微難那麼一點點,因為我得考慮位置位移。這涉及到使用者自訂介面的問題,如這個皮膚(皮膚會在以後提到)的首頁上顯示的捷徑的位置跟預設的首頁是不一樣的:
所以在做點擊檢定的時候要把不同的個人化因素及其它位置位移的因素給考慮進去,不能將點擊檢定的那個矩形寫死在代碼中。
6.2,點擊及拖拽
對於點擊,只要用過電子產品,都不會不知道,比如Windows的最基本的控制項之一的按鈕,你滑鼠按下去,抬起來,就是一個點擊。
點擊在不同場合下意義有很大不同,比如在案頭上點擊一個表徵圖是選擇這個表徵圖,實際上還包括了其它很多你想不到的動作,比如取消對其它表徵圖的選擇,如果有的話;關閉操作功能表,如果有的話;使得使用中視窗失去焦點,如果有的話,如果點擊的表徵圖已經被選中,那再次點擊它的文本區則是重新命名……好多規則,真正做過UI的軟體工程師應該都瞭解過這些細節,做UI我個人認為是相當不容易的,細節太多,外人看起來是那麼理所當然的東西內部卻蘊含了太多的錯綜複雜的邏輯,而且老闆還不會聽你去解釋那些東西……除了技術本身之外,UI還是誰都想插上一腳的地方,這進一步加大了UI開發的難度——我在這裡“吐槽”一下應該無傷大雅吧,呵呵,好吧,言歸正傳……
點擊動作在行動裝置上通常表示一個“啟動”,或者“選中”,例如你在iPhone的案頭上點擊一個表徵圖,就啟動了一個App,因為在行動裝置上要用手指完成類似滑鼠的“雙擊”是很困難的事。(你見過帶滑鼠的手機嗎?Samsung i780就是帶滑鼠的,那玩意兒還真可以輕鬆“雙擊”)那如何將一個手指按下去,再抬起來的動作檢定為一個“點擊”?通常是:按下去,沒有移動,就直接抬起來,這是一個點擊。但,不移動,那是很難的,在觸屏上,你按下之後,手指不可避免地會出現輕微的移動,即便你沒感覺到,那這個動作就變成了一個“拖拽”,而不是點擊了,所以,必須設定一個“最小拖拽距離”檢定,如果小於這個距離,就不算拖拽,還是算點擊。
(點擊/拖拽檢定)
如,手指按下處上下左右這個方形地區,是非拖拽檢定區,手指在這個地區內發生的輕微的移動不算拖拽,也不會在介面上顯示出拖拽效果,如果這個時候把手指放開,那就判定為一個點擊動作,只有手指移動超出了這個地區才算拖拽,算作拖拽之後,直到手指放開都一直認為是拖拽狀態。
這個最小拖拽距離是多少呢?不同的DPI,這個值也要作對應的調整,192DPI是72像素,128DPI是48像素,96DPI是36像素,看吧,都是為了適用不同的解析度,工作量還真不小。
6.3,蘋果引發的UI設計革命
說實在的,在蘋果之前,我做夢也不會想到UI可以這麼做,如今你看看滿大街的蘋果安卓,一切都習以為常了,不就這麼一個介面嗎?可如果蘋果不先把這樣的介面做出來,你能想得到?就像牛頓第一定律,很簡單對吧,我相信你初中就學會了,但如果不是牛頓先提出來,你敢說你能自己領悟出來嗎?
最具革命性的當屬“劃屏”這個操作,你需要滾動視窗的內容,在Windows下的做法是怎樣?使用右側的捲軸或者用滑鼠滾輪對吧,手機上通常是沒有滑鼠的,更別說滾輪了,那隻能用捲軸,我一點都不認為這個有什麼問題啊,看Windows Mobile 6.5之前的版本,不都這麼來的嗎?
(Windows Mobile的捲軸)
這個捲軸預設是很細的,我常常點不準,所幸的是我可以通過修改註冊表來調整其寬度,現在看來,這是多麼糟糕的使用者體驗啊!蘋果一出,一切都變了,你要滾動視窗內容對吧,直接用手指在上面拖拽就行了。試試看?而且還有慣性效果,手指放開後,視窗內容還繼續滾動哦……我第一次看到這個的時候覺得很是震撼,誰想出來的!
我初步想了想:如果手指碰到的地方正好是空白處,應該沒什麼問題,但如果手指碰到了表徵圖呢?會不會像Windows案頭上點中一個表徵圖然後拽到別的位置去?事實上,蘋果設計了一套全新概念的操作方式,“劃屏”這個動作不會引起我所擔心的拖拽表徵圖的結果,放心劃就是了,拖拽表徵圖有另外的操作方法。蘋果的發明幾乎成了之後的觸屏操作的標準,無數軟體開發商和裝置製造商都借鑒了蘋果的設計(包括SoSoPi)。微軟直到Windows Mobile 6.5才加入類似的功能,而且做得很“應付式”,這是它迅速沒落的一個原因。
備忘:拖拽表徵圖這種操作方式其實也是蘋果發明並最先使用於其Macintosh產品中,比微軟的Windows更早
6.4,慣性、加速度和彈簧
什麼是慣性,什麼是加速度?——不要用初中學的物理知識來回答,直接用圖:
手指鬆開,滾動仍然繼續,因為“慣性”,而手指鬆開後捲動速度會慢下來直到停止,因為“加速度”。一切看起來都是那麼自然,但我們如何告訴手機繪製這樣的效果呢?
首先,要得知手指的速度,準確說是手指離開螢幕那一瞬間的速度,這個怎麼算?觸屏可沒有能力直接給出這個速度啊,我是根據手指的移動軌跡來的,我在全域範圍內定義了一個有限隊列,這個隊列最多隻有20個元素,如果超過了20個元素,那最先進去的元素就被移除,這個隊列其實是一個靜態數組,效率是非常高的。我用這個隊列來記錄所有的“WM_MOUSEMOVE”事件的座標,(手指移動,其實也就是產生“WM_MOUSEMOVE”事件),也就是說我將最近的20個手指移動的座標都給記錄下來,發生的時間(毫秒)也記錄下來,用這些座標和時間(其實關鍵是分析最後的兩個點的座標和時間)來分析出使用者手指的速度。值得注意的是:x軸(橫向)的速度和y軸(縱向)的速度是分開計算的。例如:x軸速度為正,y軸速度為負的話表明使用者是向右下方向劃的。
速度出來了,如果沒有加速度,那麼介面就會一直滾動,直到盡頭,加速度也就是給這個速度做一個“衰減”(加速度為負嘛),讓它每次減少一點,直到速度變為0,表面上看這並不是什麼難做的功能,事實上卻需要你經過許多次校對才有比較理想的操作感,這個只有實際用上許多會才能體會,但即便我經過了許多的校正,不少使用者還是反映不夠好,每個人的感覺都不太一樣嘛,最後我還把這個做成了可配置的,我提供了一個“動畫調整”的介面,供使用者自行調整:
這個介面沒有使用貼圖,上面的東西都是直接用線條和文字繪出來的。
最後說一下彈簧,先看看什麼是彈簧:
- 第一種情況是,慣性滾動的時候滾動到頭了,我們並不立即停止,而是讓內容滾動“過頭”一點,再彈回去;
- 第二種情況是,使用者把滾動的內容拉過頭了,鬆手的時候我們得讓內容彈回去;
大家不難想到,第二種情況其實就是第一種情況的“再彈回去”,所以設計程式的時候是按照第一種情況去考慮。
彈簧分為兩個階段:
- 第一個階段是減速,比前面提到的減速要快得多,滾動時候的那個減速是相對較慢的,而彈簧的減速則很快,我在程式中是將加速度設為滾動時候的8倍,這樣“過頭”的滾動就能快速停止,當速度變為0的時候,進入第二階段;
- 第二階段是回彈,回彈的過程也是一個帶減速的過程,開始回彈的時候我會根據當前的“過頭”的距離計算出一個回彈速度,“過頭”距離越大,回彈速度越快,類比實際上彈簧的效果嘛,再給它一個反方向的加速度,基本上效果就出來了。
彈簧效果同樣經過了大量的調整,才讓老吳感覺比較“爽”,而代碼中卻早已充滿了許多“修正係數”……做UI可真不容易啊!對了,所有這些效果的實現都得考慮DPI。
6.5,動畫過程實現
提一下具體的實現,也許很多人都想到了,對,就是用Timer,手指鬆開之後,就設定一個20ms間隔的Timer來重繪介面,根據速度、加速度及狀態來計算繪製的位移量,並適當調整每次的速度、加速度及狀態,直到動畫過程結束,結束Timer。
如果分析到細節,那就更複雜一些。
考慮這種情況:滾動的過程中使用者再次按下螢幕,貌似說:“停!”,這時候是不是得馬上停下來?是的,這時候得讓Timer結束掉,當使用者再次抬起手指時候,再重新啟用Timer,如果再進一步,你會發現更多的問題,因為程式中的Timer可能不止一個,而且可能都同時參與了繪製,比如你在介面上顯示時鐘的話,那估計你得隔個幾秒鐘得檢查一次時間的分鐘是否發生了變化,如果是,那就重繪介面,但同時這個時候你的手指正按在螢幕上滾動著其中的內容,這樣很容易引起混亂,雖然這種混亂瞬現即逝,但總歸給人感覺不太好。
再例如:手指鬆開後螢幕的內容正在慣性地滾動,停止前你手指按下了滑塊區的滑塊,慣性滾動是不是得停止?如果你切換了模組,Timer應該被終止,但如果你沒有切換模組,而是直接鬆開手指,那麼前面的慣性滾動是否要繼續?如果回答都是“是”,那麼我就得讓Timer“暫停”,而不是終止,對吧?說到這裡我想你大致應該理解了“外人看起來是那麼理所當然的東西內部卻蘊含了太多的錯綜複雜的邏輯”這句話的意思。
(慣性滑動的“暫停”)
更糟糕的情況是有極小的機率會出現死結,我至今也沒修正這個bug。
用手指帶動螢幕內容的滾動是一方面,還有另一種動畫是連貫的,如日曆的翻頁:
(日曆翻頁效果)
這種動畫我們是不希望中間受到打斷的,我稱之為“原子動畫”,在原子動畫過程中,一切使用者螢幕操作將會被忽略。
解決這些使用者操作和UI渲染的問題是相當繁瑣的,我現在也在想是否有一種更清晰的思路來實現這些繪製?我不太清楚,但現在開發UI大多時候都不需要考慮這些,因為開發包裡有專門的針對動畫效果的解決方案,不必自己用一個Timer去一點點繪製。
6.6,滾屏的繪製方式
考慮到大多數軟體都是上下滾屏,(比如網頁,通常只需要垂直滾動查看,而不需要水平滾動)SoSoPi也採用了這種上下滾屏的機制。現在,關鍵問題是如何提高繪圖的效率?
方法雖多,但思路也就那麼一種:減少繪製。
如:假設你螢幕上的內容的高度是10000,而你的螢幕的實際高度是800,當前的位移量是“-5600”,那你實際上要繪製的東西是從5600這個高度到6400這個高度的範圍,如果全部繪製,那當然慢得離譜了,這就是基本的思路。
現在再具體一點,究竟從哪裡開始繪製?5600到6400?恐怕不是這樣,因為我們通常要繪製文字,文字有行高,我們只能一行行繪製,沒辦法繪製半個字或者1/3個字,假設行高是100,要繪製的文字的位置是5550,也就是說,文字雖然在5600之前,但卻要把一部分給顯示出來,那這行文字是否應該繪製?——那是肯定的,別說一半,就是只顯示一個像素,也得把它繪製出來,所以我們的方法就轉變為:判斷哪些元素會顯示在介面上,把這些要顯示的元素且只把這些要顯示的元素繪製出來。這是:
(滾屏繪製)
這種方法貫穿了整個SoSoPi,那就是:只畫需要顯示的。
(待續……)