【五线谱上确定音的高低的记号叫做谱号,谱号有三种: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 getGestureEntries() {
return mStore.getGestureEntries();
}
public ArrayList 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 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 mContext;
private final int mResourceId;
public ResourceGestureLibrary(Context context, int resourceId) {
mContext = new WeakReference(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: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 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 listeners = mOnGesturingListeners;
int count = listeners.size();
for (int i = 0; i < count; i++) {
listeners.get(i).onGesturingStarted(this);
}
在函数最后发现了onGesture函数的回调代码代码如下:
final ArrayList 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 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 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 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 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 mGestures;
public GestureCommandRegister() {
mGestures = new HashMap();
}
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手势部分代码讲解完毕。