【五线谱上确定音的高低的记号叫做谱号,谱号有三种:G谱号(高音谱号)、F谱号(低音谱号)和C谱号(中音谱号)】
--- 《五线谱基础教程》
Jamendo的手势操作用于控制歌曲的播放,有四个手势分别对应:播放,暂停,上一首和下一首。本文主要讲解手势操作部分的实现,至于涉及到媒体播放器的部分,将不作细讲,放在后续文章中。
一 Android手势操作API
Android提供GestureLibrary代表手势库,同时提供工具类GestureLibraries从不同数据源来创建手势库。其中GestureLibrary是一个抽象类,它的子类有两个,分别是FileGestureLibrary和ResourceGestureLibrary,从名字可以看出这两个手势库的数据来源,分别是文件和Android raw资源。这两个类作为GestureLibraries类的私有内部类实现。
GestureLibrary类实际上是对另一个类即GestureStore的一层封装,下面来看下GestureLibrary的代码:
public abstract class GestureLibrary { protected final GestureStore mStore; protected GestureLibrary() { mStore = new GestureStore(); } // 保存手势库 public abstract boolean save(); // 加载手势库 public abstract boolean load(); public boolean isReadOnly() { return false; } /** @hide */ public Learner getLearner() { return mStore.getLearner(); } public void setOrientationStyle(int style) { mStore.setOrientationStyle(style); } public int getOrientationStyle() { return mStore.getOrientationStyle(); } public void setSequenceType(int type) { mStore.setSequenceType(type); } public int getSequenceType() { return mStore.getSequenceType(); } public Set<String> getGestureEntries() { return mStore.getGestureEntries(); } public ArrayList<Prediction> recognize(Gesture gesture) { return mStore.recognize(gesture); } public void addGesture(String entryName, Gesture gesture) { mStore.addGesture(entryName, gesture); } public void removeGesture(String entryName, Gesture gesture) { mStore.removeGesture(entryName, gesture); } public void removeEntry(String entryName) { mStore.removeEntry(entryName); } public ArrayList<Gesture> getGestures(String entryName) { return mStore.getGestures(entryName); } }
两个子类主要实现了父类的两个抽象函数,以及重写了isReadOnly函数,分别对应不同的数据来源,但最终都是将数据转换成流的形式传给GestureStore实例的函数,由它完成最终的操作,下面看代码就清楚了:
// 从File数据源加载手势库的类 private static class FileGestureLibrary extends GestureLibrary { private final File mPath; public FileGestureLibrary(File path) { mPath = path; } @Override public boolean isReadOnly() { return !mPath.canWrite(); } public boolean save() { // 手势库没有发生变化,就不需要重新保存 if (!mStore.hasChanged()) return true; final File file = mPath; final File parentFile = file.getParentFile(); // 第一次保存时,保证文件路径的存在 if (!parentFile.exists()) { if (!parentFile.mkdirs()) { return false; } } boolean result = false; try { file.createNewFile(); mStore.save(new FileOutputStream(file), true); // 实际的保存操作 result = true; } catch (FileNotFoundException e) { Log.d(LOG_TAG, "Could not save the gesture library in " + mPath, e); } catch (IOException e) { Log.d(LOG_TAG, "Could not save the gesture library in " + mPath, e); } return result; } public boolean load() { boolean result = false; final File file = mPath; if (file.exists() && file.canRead()) { try { // 调用GestureStore实例的函数加载手势库文件 mStore.load(new FileInputStream(file), true); result = true; } catch (FileNotFoundException e) { Log.d(LOG_TAG, "Could not load the gesture library from " + mPath, e); } catch (IOException e) { Log.d(LOG_TAG, "Could not load the gesture library from " + mPath, e); } } return result; } } // 从Android的res/raw目录加载对应的手势库文件 private static class ResourceGestureLibrary extends GestureLibrary { private final WeakReference<Context> mContext; private final int mResourceId; public ResourceGestureLibrary(Context context, int resourceId) { mContext = new WeakReference<Context>(context); mResourceId = resourceId; } @Override public boolean isReadOnly() { // raw目录下的手势库文件是只读的 return true; } // 这种数据源只能加载不能保存 public boolean save() { return false; } public boolean load() { boolean result = false; final Context context = mContext.get(); if (context != null) { final InputStream in = context.getResources().openRawResource(mResourceId); try { mStore.load(in, true); result = true; } catch (IOException e) { Log.d(LOG_TAG, "Could not load the gesture library from raw resource " + context.getResources().getResourceName(mResourceId), e); } } return result; } }
Android手势API除了以上两个类外,还有一个继承自FrameLayout的GestureOverlayView类,用户可以方便地在该组件上面绘制各种手势。由于GestureOverlayView不是标准的视图组件,因此在xml布局文件中使用时,需要指定全限定的类名,如下所示:
<android.gesture.GestureOverlayView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/gestures" android:layout_width="fill_parent" android:layout_height="fill_parent" android:eventsInterceptionEnabled="false" android:gestureStrokeType="multiple" android:orientation="vertical" >
其中android:gestureStrokeType属性用于控制手势是一笔完成还是需要多笔完成,single表示单笔完成,multiple表示多笔完成。
GestureOverlayView中定义了三个监听器接口,方便用户在手势操作的各个阶段进行响应,它们定义如下:
public static interface OnGesturingListener { void onGesturingStarted(GestureOverlayView overlay); void onGesturingEnded(GestureOverlayView overlay); } public static interface OnGestureListener { void onGestureStarted(GestureOverlayView overlay, MotionEvent event); void onGesture(GestureOverlayView overlay, MotionEvent event); void onGestureEnded(GestureOverlayView overlay, MotionEvent event); void onGestureCancelled(GestureOverlayView overlay, MotionEvent event); } public static interface OnGesturePerformedListener { void onGesturePerformed(GestureOverlayView overlay, Gesture gesture); }
想要知道上面各个接口函数的实际意义,还得从GestureOverlayView源码入手,用户在View上的触摸事件可分为三步,依次是MotionEvent.ACTION_DOWN、MotionEvent.ACTION_MOVE和MotionEvent.ACTION_UP,在GestureOverlayView类中相应的处理函数分别是touchDown、touchMove和touchUp。
1)在touchDown函数中可以看到如下一段代码:
final ArrayList<OnGestureListener> listeners = mOnGestureListeners; final int count = listeners.size(); for (int i = 0; i < count; i++) { listeners.get(i).onGestureStarted(this, event); }
这里发现我们的onGestureStarted接口函数,因此,它是第一个被调用在,即在用户按下的时候。
2)在touchMove函数中的代码,首先发现onGesturingStarted函数:
final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners; int count = listeners.size(); for (int i = 0; i < count; i++) { listeners.get(i).onGesturingStarted(this); }
在函数最后发现了onGesture函数的回调代码代码如下:
final ArrayList<OnGestureListener> listeners = mOnGestureListeners; final int count = listeners.size(); for (int i = 0; i < count; i++) { listeners.get(i).onGesture(this, event); }
3)最后在touchUp函数中依次发现onGestureEnded和onGesturingEnded被调用:
final ArrayList<OnGestureListener> listeners = mOnGestureListeners; int count = listeners.size(); for (int i = 0; i < count; i++) { listeners.get(i).onGestureEnded(this, event); } clear(mHandleGestureActions && mFadeEnabled, mHandleGestureActions && mIsGesturing, false); ... final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners; int count = listeners.size(); for (int i = 0; i < count; i++) { listeners.get(i).onGesturingEnded(this); }
这样一来,已经发现的接口函数调用顺序依次是onGestureStarted、onGesturingStarted、onGesture、onGestureEnded和onGesturingEnded,还差onGestureCancelled和onGesturePerformed两个。首先来查找onGestureCancelled函数,它在私有函数cancelGesture中被调用,代码跟其他接口函数类似:
private void cancelGesture(MotionEvent event) { // pass the event to handlers final ArrayList<OnGestureListener> listeners = mOnGestureListeners; final int count = listeners.size(); for (int i = 0; i < count; i++) { listeners.get(i).onGestureCancelled(this, event); } clear(false); }
而这个私有函数则只在touchUp中被调用,而且onGestureCancel和onGestureEnded是二选一的关系,因为当手势操作被取消时,就自然不存在onGestureEnded的情况了。但是onGesturingEnded函数会紧随onGestureCancel被调用。
最后的接口函数onGesturePerformed被内部类FadeOutRunnable间接调用,而这个Runnable类是在GestureOverlayView类的clear函数中使用postDelayed以异步延迟消息的形式发送到消息程序的消息队列中去。clear函数顾名思义就是用来清除手势的,它在touchUp继onGestureEnded之后被调用,并且手势操作被取消时,onGesturePerformed依然会被回调,见上面touchUp的代码段。
因此,最终得到的接口函数调用顺序如下图所示:
二、基于命令模式(Command Pattern)的手势控制
先说下命令模式,它是将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。命令模式的典型结构图如下所示:
主要涉及四种角色:
1)接收者角色Receiver:真正执行命令的对象,任何一个类都可以成为接收者,只要他能完成命令要求实现的相应功能。
2)命令角色Command:实际包括一个Command接口和一个或者多个实现Command接口的具体命令ConcreteCommand类。实际命令对象通常持有Receiver的一个引用,并调用Receiver提供的功能来完成命令要执行的操作。
3)调用者角色Invoker:通常持有命令对象,并要求命令对象执行相应的请求,Invoker相当于是使用命令对象的入口。
4)装配者角色Client,用于创建具体的命令对象,并设置命令对象的接收者。
下面就参照命令模式的结构图,来看看Jamendo的手势操作实现代码,大部分定义在gesture包中,如图
从类的名字可以大概看出各个类的责任分派:
1)抽象命令角色Command:GestureCommand接口,定义简单,整个接口只有一个execute函数:
public interface GestureCommand { void execute(); }
2)具体命令角色ConcreteCommand:PlayerGestureNextCommand、PlayerGesturePlayCommand、PlayerGesturePrevCommand、PlayerGestureStopCommand类,在这四个具体实现类中,都持有接收者角色PlayerEngine的实例,由它来完成具体的控制操作,以播放下一首这个命令为例,看下代码:
public class PlayerGestureNextCommand implements GestureCommand { PlayerEngine mPlayerEngine; //持有Receiver角色的一个引用 // 将Receiver作为构造函数的参数传入,当然也可以使用Setter的方式传入,更灵活一些 public PlayerGestureNextCommand( PlayerEngine engine ){ mPlayerEngine = engine; } @Override public void execute() { Log.v(JamendoApplication.TAG, "PlayerGestureNextCommand"); mPlayerEngine.next(); // 由Receiver完成具体的命令操作 } }
3)命令接收者角色Receiver:PlayerEngine本身是一个接口,定义了音频播放操作的一些通用函数,它有两个实现类,分别是IntentPlayerEngine和PlayerEngineImpl,这一部分放在后面文章分析,因为不属于本文的手势主题。
4)调用者角色Invoker:GestureHandler类,这个类是实现手势控制的关键类,它实现了OnGesturePerformedListener监听器,并重写了onGesturePerformed接口函数,在这个函数中首先判断手势库是否已经加载,在确保手势库正确加载的情况下,调用GestureLibrary.recognize进行手势识别,并在匹配度大于2.0时才确认识别到了预先定义的手势;手势识别到之后,就调用命令接口定义的execute函数,进行音乐播放的控制,代码如下:
public class GesturesHandler implements OnGesturePerformedListener { private GestureLibrary mLibrary; // 手势库 private boolean mLoaded = false; private GestureCommandRegister mRegister; // 保存了已注册的控制命令(包括next、prev、play、stop) public GesturesHandler(Context context, GestureCommandRegister register) { // 从res/raw目录下加载gestures手势库 mLibrary = GestureLibraries.fromRawResource(context, R.raw.gestures); load(); setRegister(register); } /** * 加载手势库 * @return */ private boolean load() { mLoaded = mLibrary.load(); return mLoaded; } @Override public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture) { if (!mLoaded) { if (!load()) { return; } } // 识别用户输入的手势 ArrayList<Prediction> predictions = mLibrary.recognize(gesture); if (predictions.size() > 0) { Prediction prediction = predictions.get(0); Log.v(JamendoApplication.TAG, "Gesture " + prediction.name + " recognized with score " + prediction.score); if (prediction.score > 2.0) { // 当手势匹配度大于2.0时,确定识别到了事先定义好的手势 // 调用先前注册的命令接口定义的操作 GestureCommand command = getRegister().getCommand( prediction.name); if (command != null) { command.execute(); } } } } public void setRegister(GestureCommandRegister mRegister) { this.mRegister = mRegister; } public GestureCommandRegister getRegister() { return mRegister; } }
5)装配者角色Client:由GestureCommandRegister类和PlayGestureCommandRegister类共同充当Client角色,GestureCommandRegister类维护了一个HashMap,用于充当各种具体命令角色的容器;PlayGestureCommandRegister是GestureCommandRegister的子类,主要实现注册next、prev、play和stop这四种具体播发控制命令角色的功能。
public class GestureCommandRegister { private HashMap<String, GestureCommand> mGestures; public GestureCommandRegister() { mGestures = new HashMap<String, GestureCommand>(); } public void registerCommand(String name, GestureCommand gestureCommand) { mGestures.put(name, gestureCommand); } public GestureCommand getCommand(String name) { return mGestures.get(name); } } public class PlayerGestureCommandRegiser extends GestureCommandRegister { public PlayerGestureCommandRegiser(PlayerEngine playerEngine) { super(); registerCommand("next", new PlayerGestureNextCommand(playerEngine)); registerCommand("prev", new PlayerGesturePrevCommand(playerEngine)); registerCommand("play", new PlayerGesturePlayCommand(playerEngine)); registerCommand("stop", new PlayerGestureStopCommand(playerEngine)); } }
至此,Jamendo手势部分代码讲解完毕。