Android Gesture 手势研究
怎么理解一個(gè)手勢(shì),就是在屏幕上,手畫一個(gè)符號(hào)就是一個(gè)手勢(shì),它代表了用戶的一個(gè)意圖,也就是用戶希望程序做點(diǎn)什么,一般程序大多數(shù)是通過按鈕,按鈕上有對(duì)應(yīng)的文字,這樣進(jìn)行人機(jī)交互,而手勢(shì)也是很多地方會(huì)使用到,而常用的手勢(shì)好像下拉刷新,用戶希望列表內(nèi)容下拉一下就有新的信息,雙指縮放等等,一般這些手勢(shì)都是跟對(duì)應(yīng)的view綁定起來,而今天介紹的都是方法是可以不綁定view,直接在界面上畫一個(gè)手勢(shì)就可以人機(jī)交互.實(shí)現(xiàn)的代碼可以在github上的Demo源碼了解.
這篇手勢(shì)研究會(huì)大概分三部分
1. 手勢(shì)Gesture使用方式
2. 展示手勢(shì)開發(fā)的步驟及代碼實(shí)現(xiàn)
3. 分析Gesture的源碼及原理
使用的方式
首先我們需要把用戶需要使用到的手勢(shì)提前記錄下來,準(zhǔn)備一些手勢(shì)的樣本,在app安裝時(shí)隨著資源文件或者下載等方式存儲(chǔ)到用戶的手機(jī)里,當(dāng)用戶在app畫一個(gè)手勢(shì)時(shí),就去匹配手勢(shì)樣本,當(dāng)時(shí)樣本最吻合時(shí),就知道用戶的意圖,采取執(zhí)行對(duì)應(yīng)的功能,這樣就是個(gè)很好的人機(jī)交互的方式.
從上文使用方式,我們大概猜想到,我們需要一個(gè)東西,用來管理和讀取我們已經(jīng)存儲(chǔ)的手勢(shì)樣本,我們還要需要這個(gè)東西可以設(shè)別用戶的手勢(shì)跟我們已經(jīng)存儲(chǔ)的手勢(shì)進(jìn)行匹配.還有,我們需一個(gè)東西在app的界面上記錄用戶的手勢(shì),沒錯(cuò),兩個(gè)東西都存在,就是GestureLibrary和GestureOverlayView,這兩個(gè)類就是手勢(shì)開發(fā)里使用的主要兩個(gè)類,通過這兩個(gè)類,我們就可以實(shí)現(xiàn)手勢(shì)開發(fā)的所有功能,是不是很簡(jiǎn)單.
總結(jié)一下:
1. 提前準(zhǔn)備好手勢(shì)樣本,在安裝時(shí)加入到資源文件或者安裝后網(wǎng)絡(luò)下載.
2. 需要使用手勢(shì)的界面里使用GestureOverlayView記錄用戶的手勢(shì),
3. 使用GestureLibrary對(duì)象對(duì)用戶的手勢(shì)進(jìn)行監(jiān)聽和匹配,找到用戶手勢(shì)的意圖,執(zhí)行對(duì)應(yīng)的功能
步驟及代碼實(shí)現(xiàn)
手勢(shì)庫的初始化
GestureLibrary gLib=GestureLibraries.fromFile(手勢(shì)庫文件);
gLib.load();
這個(gè)過程是讀取已經(jīng)存儲(chǔ)手勢(shì)樣本文件,構(gòu)造出GestureLibrary實(shí)例的過程,需要第一步實(shí)現(xiàn).
對(duì)用戶手勢(shì)的監(jiān)聽
GestureOverlayView.addOnGesturePerformedListener()
使用GestureLibrary對(duì)用戶的手勢(shì)進(jìn)行匹配
recognize(Gesture gesture)
score越高代表越匹配.
Prediction.score()
這里就是手勢(shì)開發(fā)的實(shí)現(xiàn)的全部?jī)?nèi)容,但是作為一個(gè)程序猿,需要知其然知其所以然,就要對(duì)源碼進(jìn)行解剖.
原理
手勢(shì)的結(jié)構(gòu)
手勢(shì)是用戶在屏幕上畫的符號(hào),那么手勢(shì)可以簡(jiǎn)單的一筆筆畫,例如一個(gè)方向的箭頭(>),也可以多筆劃,很復(fù)雜,例如一個(gè)文字.這些都手勢(shì),所以我們就知道
手勢(shì)是由一個(gè)或者多個(gè)筆畫組成
學(xué)過數(shù)學(xué)的我們都到線是由點(diǎn)組成的,所以
一個(gè)手勢(shì)筆畫是由多個(gè)時(shí)間連續(xù)的點(diǎn)組成
一個(gè)點(diǎn)意味著什么呢,它會(huì)固定在屏幕的某個(gè)地方,還需要時(shí)間連續(xù)不斷,所以
手勢(shì)中的點(diǎn)包含坐標(biāo)X軸和Y軸,還有時(shí)間戳
所以我們就很容易了解手勢(shì)對(duì)應(yīng)的文件了
GesturePoint : 是手勢(shì)筆劃中的一個(gè)點(diǎn),包含X軸,Y軸的坐標(biāo),還有時(shí)間戳.
GestureStroke : 手勢(shì)筆劃,可以理解為線,由多個(gè)點(diǎn)組成的.
Gesture : 手勢(shì),代表用戶的一個(gè)手勢(shì),可以由一個(gè)或者多個(gè)手勢(shì)筆劃組成.
GestureStore 手勢(shì)倉庫,里面存儲(chǔ)了多個(gè)手勢(shì)樣本
手勢(shì)的使用
使用手勢(shì)的過程都是先從GestureLibrary開始,那么看看GestureLibrary的關(guān)系圖.
從圖中看,GestureLibrary的實(shí)現(xiàn)有兩種,一個(gè)File的實(shí)現(xiàn),另外一個(gè)是由資源Resource實(shí)現(xiàn),說明我們的手勢(shì)庫可有兩個(gè)方向可以構(gòu)造.
然后看回GestureLibrary的源碼
public abstract class GestureLibrary {protected final GestureStore mStore;... }里面只有一個(gè)對(duì)象,而所有的方法都是由這個(gè)對(duì)象實(shí)現(xiàn),也就是GestureLibrary其實(shí)是GestureStore的代理類,而真正的功能其實(shí)是在GestureStore里.
GestureStore的內(nèi)容很多,首先看到的是頂部注釋里有手勢(shì)文件的結(jié)構(gòu)內(nèi)容
| Header | ||
| 2 bytes | short | |
| 4 bytes | int | |
| Entry | ||
| X bytes | UTF String | |
| 4 bytes | int | |
| Gesture | ||
| 8 bytes | long | |
| 4 bytes | int | |
| Stroke | ||
| 4 bytes | int | |
| Point | ||
| 4 bytes | float | |
| 4 bytes | float | |
| 8 bytes | long |
從源碼可以知道,GestureStore的文件格式主要組成部分,也就是GestureLibrary讀取文件的格式內(nèi)容,也可以考慮根據(jù)這樣的格式來進(jìn)行加密,假如用手勢(shì)來做成一個(gè)手寫輸入法的軟件,那么手勢(shì)庫一定是龐大的內(nèi)容庫,而且根據(jù)所有人不同的手寫方式,這樣的手勢(shì)庫一定很有價(jià)值,至于怎樣加密來保護(hù)這些價(jià)值,就可以考慮每個(gè)手勢(shì)的內(nèi)容進(jìn)行拆分來分別存儲(chǔ)和采取不同的加密方式加密.
然后我們?cè)倏碨tore對(duì)手勢(shì)的讀取保存
讀取和保存
讀取第一步GestureLibraries中讀取手勢(shì)文件
public boolean load() {...mStore.load(new FileInputStream(file), true);... }第二步store獲取文件流
public void load(InputStream stream, boolean closeStream) throws IOException {DataInputStream in = null;try {in = new DataInputStream((stream instanceof BufferedInputStream) ? stream :new BufferedInputStream(stream, GestureConstants.IO_BUFFER_SIZE));...// Read file format version numberfinal short versionNumber = in.readShort();switch (versionNumber) {case 1:readFormatV1(in);break;}...}第三步從文件流里讀取文件名和手勢(shì)對(duì)象(Gestire),然后存進(jìn)HashMap里
/*** 讀取文件數(shù)據(jù)** @param in* @throws IOException*/private void readFormatV1(DataInputStream in) throws IOException {...for (int i = 0; i < entriesCount; i++) {// Entry namefinal String name = in.readUTF();// Number of gesturesfinal int gestureCount = in.readInt();final ArrayList<Gesture> gestures = new ArrayList<Gesture>(gestureCount);for (int j = 0; j < gestureCount; j++) {final Gesture gesture = Gesture.deserialize(in);gestures.add(gesture);classifier.addInstance(Instance.createInstance(mSequenceType, mOrientationStyle, gesture, name));}namedGestures.put(name, gestures);} }讀取的方式是從文件流里獲取到手勢(shì)數(shù)據(jù),從Gesture的deserialize方法可以知道,每一步的解析都是按照文件存儲(chǔ)格式一步步獲取數(shù)據(jù),當(dāng)然,存儲(chǔ)也是反向一步步保存成文件流格式存儲(chǔ)的.
手勢(shì)的匹配
這里我們?cè)俸煤锰角笫謩?shì)的設(shè)別匹配,也是我認(rèn)為手勢(shì)源碼之中最有研究?jī)r(jià)值的一塊.當(dāng)把代碼解析一下就會(huì)發(fā)現(xiàn)其實(shí)很多功能的本質(zhì)就是數(shù)學(xué)問題,而這里的手勢(shì)匹配的本質(zhì)就是數(shù)學(xué)的線性代數(shù).
首先從匹配的方法入手,GestureStore.recognize()方法開始看
public ArrayList<Prediction> recognize(Gesture gesture) {//實(shí)例Instance instance = Instance.createInstance(mSequenceType, mOrientationStyle, gesture, null);//歸類return mClassifier.classify(mSequenceType, mOrientationStyle, instance.vector);}recognize()方法里有兩個(gè)核心,一個(gè)是根據(jù)手勢(shì)對(duì)象(Gesture)來構(gòu)造一個(gè)實(shí)例,二是通過mClassifier對(duì)象的classify()方法來返回一個(gè)Prediction數(shù)組.
首先從Instance來研究.
static Instance createInstance(int sequenceType, int orientationType, Gesture gesture, String label) {float[] pts;Instance instance;if (sequenceType == GestureStore.SEQUENCE_SENSITIVE) {//單筆手勢(shì)//得到一個(gè)連續(xù)點(diǎn)的數(shù)組pts = temporalSampler(orientationType, gesture);instance = new Instance(gesture.getID(), pts, label);instance.normalize();} else {pts = spatialSampler(gesture);instance = new Instance(gesture.getID(), pts, label);}return instance; }從Instance的構(gòu)造方法看是需要三個(gè)參數(shù),id,連續(xù)點(diǎn)數(shù)組,和標(biāo)簽label.所以temporalSampler()和spatialSampler()都是把手勢(shì)gesture轉(zhuǎn)換為一個(gè)數(shù)組.
但是為什么需要把一個(gè)手勢(shì)轉(zhuǎn)換為一個(gè)數(shù)組呢,我們都知道一條線是由無數(shù)個(gè)點(diǎn),假如點(diǎn)太多就帶來很大量的計(jì)算工作,所以我們采用生物學(xué)的抽樣法.每隔固定的間隔就取一個(gè)樣本,這樣就減少計(jì)算量,但是太少的話就會(huì)樣本集合與真實(shí)的差別就很大,所以我們?nèi)チ艘粋€(gè)適合的量作為樣本數(shù)量.
private static final int SEQUENCE_SAMPLE_SIZE = 16;
我們?nèi)×藰颖緮?shù)量為16,把任何一個(gè)手勢(shì)筆劃轉(zhuǎn)換為均勻分割的16個(gè)點(diǎn)來代替.
轉(zhuǎn)換的方法就是GestureUtils.temporalSampling()
/*** Samples a stroke temporally into a given number of evenly-distributed* points.* 代表均勻分布的點(diǎn)的一系列數(shù)字作為時(shí)間取樣的筆劃例子* 把一個(gè)手勢(shì)的筆劃(連續(xù)點(diǎn)的線)轉(zhuǎn)化為離散的點(diǎn)** @param stroke the gesture stroke to be sampled* @param numPoints the number of points 取樣點(diǎn)的數(shù)量(越多越精確,越多消耗性能越大)* @return the sampled points in the form of [x1, y1, x2, y2, ..., xn, yn]*/public static float[] temporalSampling(GestureStroke stroke, int numPoints) {//遞增量,手勢(shì)筆畫的長(zhǎng)度除以需要切開的段數(shù)(離散點(diǎn)數(shù) - 1)final float increment = stroke.length / (numPoints - 1);//向量長(zhǎng)度int vectorLength = numPoints * 2;//向量float[] vector = new float[vectorLength];//因?yàn)橄蛄烤褪侨狱c(diǎn)的內(nèi)容,包含x,y坐標(biāo),所以是取樣點(diǎn)的兩倍float distanceSoFar = 0;float[] pts = stroke.points;//上次最新的坐標(biāo)float lstPointX = pts[0];float lstPointY = pts[1];int index = 0;//當(dāng)前坐標(biāo)float currentPointX = Float.MIN_VALUE;float currentPointY = Float.MIN_VALUE;vector[index] = lstPointX;index++;vector[index] = lstPointY;index++;int i = 0;int count = pts.length / 2;while (i < count) {//默認(rèn)值,也是第一個(gè)運(yùn)行時(shí)執(zhí)行的if (currentPointX == Float.MIN_VALUE) {i++;if (i >= count) {break;}currentPointX = pts[i * 2];currentPointY = pts[i * 2 + 1];}//坐標(biāo)偏移量float deltaX = currentPointX - lstPointX;//兩個(gè)坐標(biāo)點(diǎn)的X軸差值float deltaY = currentPointY - lstPointY;//兩個(gè)坐標(biāo)點(diǎn)的Y軸差值//deltaX 和 deltaY的平方和的平方根(根據(jù)三角函數(shù),)也就是兩個(gè)點(diǎn)的直線距離float distance = (float) Math.hypot(deltaX, deltaY);//根據(jù)三角函數(shù)定理,X2 + Y2 = Z2if (distanceSoFar + distance >= increment) {//當(dāng)兩個(gè)點(diǎn)(疊加上次循環(huán)的距離)的距離大于遞增量(根據(jù)numPoints來確定的離散點(diǎn)的間隔距離)時(shí)執(zhí)行//比例float ratio = (increment - distanceSoFar) / distance;float nx = lstPointX + ratio * deltaX;float ny = lstPointY + ratio * deltaY;vector[index] = nx;index++;vector[index] = ny;index++;lstPointX = nx;lstPointY = ny;distanceSoFar = 0;} else {//當(dāng)兩個(gè)點(diǎn)的距離少于間隔距離//緩存當(dāng)前的點(diǎn)lstPointX = currentPointX;lstPointY = currentPointY;//當(dāng)前點(diǎn)默認(rèn)最小值currentPointX = Float.MIN_VALUE;currentPointY = Float.MIN_VALUE;//疊加記錄兩點(diǎn)距離distanceSoFar += distance;}}//添加剩下最后一個(gè)點(diǎn)的坐標(biāo)for (i = index; i < vectorLength; i += 2) {vector[i] = lstPointX;vector[i + 1] = lstPointY;}return vector;}其中就使用到數(shù)學(xué)的三角函數(shù)公式,通過兩個(gè)點(diǎn)的坐標(biāo)(x,y)來計(jì)算兩點(diǎn)距離.
回到Instance的類
//時(shí)間取樣private static float[] temporalSampler(int orientationType, Gesture gesture) {//離散點(diǎn)float[] pts = GestureUtils.temporalSampling(gesture.getStrokes().get(0), SEQUENCE_SAMPLE_SIZE);//重心點(diǎn)float[] center = GestureUtils.computeCentroid(pts);//計(jì)算弧度值(計(jì)算第一個(gè)點(diǎn)與重心點(diǎn)形成的角度的弧度值)float orientation = (float) Math.atan2(pts[1] - center[1], pts[0] - center[0]);//???float adjustment = -orientation;if (orientationType != GestureStore.ORIENTATION_INVARIANT) {int count = ORIENTATIONS.length;for (int i = 0; i < count; i++) {float delta = ORIENTATIONS[i] - orientation;if (Math.abs(delta) < Math.abs(adjustment)) {adjustment = delta;}}}//根據(jù)中心點(diǎn)平移,平移到中心點(diǎn)在原點(diǎn)上GestureUtils.translate(pts, -center[0], -center[1]);//根據(jù)調(diào)整出來的adjustment旋轉(zhuǎn)數(shù)據(jù)GestureUtils.rotate(pts, adjustment);return pts;}除計(jì)算adjustment的方法還沒理解透,歡迎讀者可以繼續(xù)跟我交流
這個(gè)方法主要計(jì)算出手勢(shì)的間隔點(diǎn)數(shù)組,然后平移到坐標(biāo)原點(diǎn)上和調(diào)整角度,輸出調(diào)整后的數(shù)組.就大概完成這個(gè)功能內(nèi)容.接著我們繼續(xù)看下個(gè)功能點(diǎn)classify.
mClassifier這個(gè)對(duì)象的類似Learner,就是用于實(shí)現(xiàn)匹配功能的類,而classify的實(shí)現(xiàn)類在InstanceLearner這個(gè)類里,那么到底一個(gè)這么重要的方法classify到底做了什么呢?
/*** 歸類** @param sequenceType* @param orientationType* @param vector* @return*/@OverrideArrayList<Prediction> classify(int sequenceType, int orientationType, float[] vector) {//預(yù)測(cè)對(duì)象數(shù)組ArrayList<Prediction> predictions = new ArrayList<Prediction>();//實(shí)例數(shù)組ArrayList<Instance> instances = getInstances();int count = instances.size();//便簽找到得分值的mapTreeMap<String, Double> label2score = new TreeMap<String, Double>();for (int i = 0; i < count; i++) {Instance sample = instances.get(i);//保證數(shù)據(jù)長(zhǎng)度一致if (sample.vector.length != vector.length) {continue;}//距離(與手勢(shì)的差距)double distance;if (sequenceType == GestureStore.SEQUENCE_SENSITIVE) {distance = GestureUtils.minimumCosineDistance(sample.vector, vector, orientationType);} else {distance = GestureUtils.squaredEuclideanDistance(sample.vector, vector);}//權(quán)重(權(quán)重越大,代表越匹配)double weight;if (distance == 0) {//代表完全吻合weight = Double.MAX_VALUE;} else {//取distance的倒數(shù)weight = 1 / distance;}Double score = label2score.get(sample.label);if (score == null || weight > score) {label2score.put(sample.label, weight);}}for (String name : label2score.keySet()) {double score = label2score.get(name);predictions.add(new Prediction(name, score));}//排序Collections.sort(predictions, sComparator);return predictions;}這個(gè)類主要做的事情就是對(duì)用戶的手勢(shì)和所有的已存的手勢(shì)進(jìn)行匹配,計(jì)算出相識(shí)度的權(quán)重,然后我們就可以根據(jù)這個(gè)權(quán)重來知道用戶的手勢(shì)大概是什么意思.所以這個(gè)方法最重要的內(nèi)容是計(jì)算權(quán)重的方法,GestureUtils的minimumCosineDistance()和squaredEuclideanDistance()
/*** Calculates the "minimum" cosine distance between two instances.* <p>* 最小的余弦距離** @param vector1* @param vector2* @param numOrientations the maximum number of orientation allowed* @return the distance between the two instances (between 0 and Math.PI)*/static float minimumCosineDistance(float[] vector1, float[] vector2, int numOrientations) {final int len = vector1.length;//???float a = 0;float b = 0;for (int i = 0; i < len; i += 2) {a += vector1[i] * vector2[i] + vector1[i + 1] * vector2[i + 1];//(x1 * x2 + y1 * y2)疊加所有坐標(biāo)b += vector1[i] * vector2[i + 1] - vector1[i + 1] * vector2[i];//(x1 * y2 + y1 * x2)疊加所有坐標(biāo)}if (a != 0) {final float tan = b / a;//角度final double angle = Math.atan(tan);if (numOrientations > 2 && Math.abs(angle) >= Math.PI / numOrientations) {return (float) Math.acos(a);} else {final double cosine = Math.cos(angle);final double sine = cosine * tan;return (float) Math.acos(a * cosine + b * sine);}} else {return (float) Math.PI / 2;}}minimumCosineDistance()方法從注釋來說就是實(shí)現(xiàn)最小的余弦距離,把用戶手勢(shì)點(diǎn)和一個(gè)樣本的手勢(shì)點(diǎn)進(jìn)行疊加計(jì)算,
/*** Calculates the squared Euclidean distance between two vectors.** @param vector1* @param vector2* @return the distance*/static float squaredEuclideanDistance(float[] vector1, float[] vector2) {float squaredDistance = 0;int size = vector1.length;for (int i = 0; i < size; i++) {//坐標(biāo)點(diǎn)的x軸或y軸差距float difference = vector1[i] - vector2[i];squaredDistance += difference * difference;}return squaredDistance / size;}squaredEuclideanDistance 的方法就是計(jì)算兩點(diǎn)差距,然后平方和再除以數(shù)量.
minimumCosineDistance()和squaredEuclideanDistance()的實(shí)現(xiàn)是知道,但是為什么要這樣計(jì)算,和使用哪些數(shù)學(xué)原理還需繼續(xù)深究,歡迎讀者跟我進(jìn)行探究.
到這里Gesture的初步研究就差不多了,假如讀者需要安卓源碼的部分翻譯,可以點(diǎn)擊這里獲取.
假如讀者需要閱讀GestureDemo可以點(diǎn)擊這里,假如讀者需要跟我交流github有郵箱聯(lián)系方法
本文信息:
作者:StevenHe
博客:簡(jiǎn)書 - 可樂
CSDN:可樂的小屋
工作郵箱:steven2947@163.com
請(qǐng)尊重原創(chuàng)作者,復(fù)制引用時(shí)保留作者信息
總結(jié)
以上是生活随笔為你收集整理的Android Gesture 手势研究的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 示波器的触发功能
- 下一篇: android sina oauth2.