diff options
| author | bringert <bringert@google.com> | 2013-10-04 16:26:46 +0000 |
|---|---|---|
| committer | bringert <bringert@google.com> | 2013-10-04 16:26:46 +0000 |
| commit | a35b286ce040919a71d27e5bf696886237c6c470 (patch) | |
| tree | 4e54cc1994a7f0760f76b0efe8c9e85ef38ec9ef /src | |
| parent | f156a5e8f9c903f132541e1c34f1c9f5c1ee69af (diff) | |
Add new Android voice translator sample app
This adds a simple voice translator Android app that uses
the JNI bindings to the PGF C runtime.
Caveats:
- Since the C runtime doesn't compile for Android right now,
I've bundled an old copy, along with its Java bindings.
That should be removed once the C runtime compiels for Android
again.
- Adding an automated build would be nice.
- Replacing the grammar requires editing a Java file, that should
really be more dynamic.
Diffstat (limited to 'src')
45 files changed, 1236 insertions, 147 deletions
diff --git a/src/ui/android/.classpath b/src/ui/android/.classpath index 2f27127f0..7bc01d9a9 100644 --- a/src/ui/android/.classpath +++ b/src/ui/android/.classpath @@ -3,11 +3,7 @@ <classpathentry kind="src" path="src"/> <classpathentry kind="src" path="gen"/> <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/> - <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"> - <attributes> - <attribute name="org.eclipse.jdt.launching.CLASSPATH_ATTR_LIBRARY_PATH_ENTRY" value="jni"/> - </attributes> - </classpathentry> + <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/> <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/> <classpathentry kind="output" path="bin/classes"/> </classpath> diff --git a/src/ui/android/.project b/src/ui/android/.project index abf063e43..2773e5ef0 100644 --- a/src/ui/android/.project +++ b/src/ui/android/.project @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <projectDescription> - <name>GFTranslator</name> + <name>GFVoiceExample</name> <comment></comment> <projects> </projects> diff --git a/src/ui/android/AndroidManifest.xml b/src/ui/android/AndroidManifest.xml index 9fef8e112..9728b71aa 100644 --- a/src/ui/android/AndroidManifest.xml +++ b/src/ui/android/AndroidManifest.xml @@ -2,19 +2,21 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.grammaticalframework.ui.android" android:versionCode="1" - android:versionName="1.0"> + android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" - android:targetSdkVersion="8" /> + android:targetSdkVersion="18" /> + + <uses-permission android:name="android.permission.RECORD_AUDIO" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" > <activity - android:name="org.grammaticalframework.ui.android.MainActivity" + android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> diff --git a/src/ui/android/README b/src/ui/android/README new file mode 100644 index 000000000..1d3bda521 --- /dev/null +++ b/src/ui/android/README @@ -0,0 +1,66 @@ += Overview = + +This directory contains a sample Android app tht uses +the Android speech recognition and TTS APIs along with +JNI bindings to the C PGF runtime to implement a simple +speech translation app. + + += Requirements = + +1. Android SDK: http://developer.android.com/sdk/ + installed in $ANDROID_SDK_LOCATION + +2. Android NDK: http://developer.android.com/tools/sdk/ndk/ + installed in $ANDROID_NDK_LOCATION + += Building = + +Set up Android project: + +# Creates local.properties, not to be checked in +$ $ANDROID_SDK_LOCATION/tools/android update project -p . + +Build libs/libjpgf.jar: + +$ (cd ../../runtime/java && javac org/grammaticalframework/pgf/*.java && jar -cf libjpgf.jar org/grammaticalframework/pgf/*.class) +$ cp ../../runtime/java/libjpgf.jar libs + +Build JNI code: + +$ cd jni +$ $ANDROID_NDK_LOCATION/ndk-build + + +Build APK: + +$ ant debug + + +Install on your device: + +$ ant debug install + +or: + +$ adb install -r bin/MainActivity-debug.apk + + += Changing the grammar = + +1. Replace assets/ResourceDemo.pgf + +2. Edit Translator.java to point to the new file and include its metadata + + += Developing in Eclipse = + +1. Install Android ADT + +2. Eclipse > File > Import > Existing Projects into Workspace > Next + +3. Select root directory... + +4. Select GF/src/ui/android + +5. Finish
\ No newline at end of file diff --git a/src/ui/android/assets/ResourceDemo.pgf b/src/ui/android/assets/ResourceDemo.pgf Binary files differnew file mode 100644 index 000000000..c40f38a1d --- /dev/null +++ b/src/ui/android/assets/ResourceDemo.pgf diff --git a/src/ui/android/build.xml b/src/ui/android/build.xml new file mode 100644 index 000000000..a10a91491 --- /dev/null +++ b/src/ui/android/build.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project name="MainActivity" default="help"> + + <!-- The local.properties file is created and updated by the 'android' tool. + It contains the path to the SDK. It should *NOT* be checked into + Version Control Systems. --> + <property file="local.properties" /> + + <!-- The ant.properties file can be created by you. It is only edited by the + 'android' tool to add properties to it. + This is the place to change some Ant specific build properties. + Here are some properties you may want to change/update: + + source.dir + The name of the source directory. Default is 'src'. + out.dir + The name of the output directory. Default is 'bin'. + + For other overridable properties, look at the beginning of the rules + files in the SDK, at tools/ant/build.xml + + Properties related to the SDK location or the project target should + be updated using the 'android' tool with the 'update' action. + + This file is an integral part of the build system for your + application and should be checked into Version Control Systems. + + --> + <property file="ant.properties" /> + + <!-- if sdk.dir was not set from one of the property file, then + get it from the ANDROID_HOME env var. + This must be done before we load project.properties since + the proguard config can use sdk.dir --> + <property environment="env" /> + <condition property="sdk.dir" value="${env.ANDROID_HOME}"> + <isset property="env.ANDROID_HOME" /> + </condition> + + <!-- The project.properties file is created and updated by the 'android' + tool, as well as ADT. + + This contains project specific properties such as project target, and library + dependencies. Lower level build properties are stored in ant.properties + (or in .classpath for Eclipse projects). + + This file is an integral part of the build system for your + application and should be checked into Version Control Systems. --> + <loadproperties srcFile="project.properties" /> + + <!-- quick check on sdk.dir --> + <fail + message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable." + unless="sdk.dir" + /> + + <!-- + Import per project custom build rules if present at the root of the project. + This is the place to put custom intermediary targets such as: + -pre-build + -pre-compile + -post-compile (This is typically used for code obfuscation. + Compiled code location: ${out.classes.absolute.dir} + If this is not done in place, override ${out.dex.input.absolute.dir}) + -post-package + -post-build + -pre-clean + --> + <import file="custom_rules.xml" optional="true" /> + + <!-- Import the actual build file. + + To customize existing targets, there are two options: + - Customize only one target: + - copy/paste the target into this file, *before* the + <import> task. + - customize it to your needs. + - Customize the whole content of build.xml + - copy/paste the content of the rules files (minus the top node) + into this file, replacing the <import> task. + - customize to your needs. + + *********************** + ****** IMPORTANT ****** + *********************** + In all cases you must update the value of version-tag below to read 'custom' instead of an integer, + in order to avoid having your file be overridden by tools such as "android update project" + --> + <!-- version-tag: 1 --> + <import file="${sdk.dir}/tools/ant/build.xml" /> + +</project> diff --git a/src/ui/android/libs/android-support-v4.jar b/src/ui/android/libs/android-support-v4.jar Binary files differnew file mode 100644 index 000000000..cf12d2839 --- /dev/null +++ b/src/ui/android/libs/android-support-v4.jar diff --git a/src/ui/android/libs/armeabi/libjpgf.so b/src/ui/android/libs/armeabi/libjpgf.so Binary files differnew file mode 100644 index 000000000..69014824d --- /dev/null +++ b/src/ui/android/libs/armeabi/libjpgf.so diff --git a/src/ui/android/libs/libjpgf.jar b/src/ui/android/libs/libjpgf.jar Binary files differnew file mode 100644 index 000000000..e472505d3 --- /dev/null +++ b/src/ui/android/libs/libjpgf.jar diff --git a/src/ui/android/proguard-project.txt b/src/ui/android/proguard-project.txt new file mode 100644 index 000000000..f2fe1559a --- /dev/null +++ b/src/ui/android/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/src/ui/android/res/drawable-hdpi/ic_action_switch.png b/src/ui/android/res/drawable-hdpi/ic_action_switch.png Binary files differnew file mode 100644 index 000000000..5449a32b8 --- /dev/null +++ b/src/ui/android/res/drawable-hdpi/ic_action_switch.png diff --git a/src/ui/android/res/drawable-hdpi/ic_launcher.png b/src/ui/android/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..a6c350aea --- /dev/null +++ b/src/ui/android/res/drawable-hdpi/ic_launcher.png diff --git a/src/ui/android/res/drawable-hdpi/ic_mic.png b/src/ui/android/res/drawable-hdpi/ic_mic.png Binary files differnew file mode 100644 index 000000000..f79ff489b --- /dev/null +++ b/src/ui/android/res/drawable-hdpi/ic_mic.png diff --git a/src/ui/android/res/drawable-mdpi/ic_action_switch.png b/src/ui/android/res/drawable-mdpi/ic_action_switch.png Binary files differnew file mode 100644 index 000000000..ecf7d0347 --- /dev/null +++ b/src/ui/android/res/drawable-mdpi/ic_action_switch.png diff --git a/src/ui/android/res/drawable-mdpi/ic_launcher.png b/src/ui/android/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..204c58a8f --- /dev/null +++ b/src/ui/android/res/drawable-mdpi/ic_launcher.png diff --git a/src/ui/android/res/drawable-mdpi/ic_mic.png b/src/ui/android/res/drawable-mdpi/ic_mic.png Binary files differnew file mode 100644 index 000000000..8f7f55cf9 --- /dev/null +++ b/src/ui/android/res/drawable-mdpi/ic_mic.png diff --git a/src/ui/android/res/drawable-xhdpi/ic_action_switch.png b/src/ui/android/res/drawable-xhdpi/ic_action_switch.png Binary files differnew file mode 100644 index 000000000..b5da00fb2 --- /dev/null +++ b/src/ui/android/res/drawable-xhdpi/ic_action_switch.png diff --git a/src/ui/android/res/drawable-xhdpi/ic_launcher.png b/src/ui/android/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..7f212cc6b --- /dev/null +++ b/src/ui/android/res/drawable-xhdpi/ic_launcher.png diff --git a/src/ui/android/res/drawable-xhdpi/ic_mic.png b/src/ui/android/res/drawable-xhdpi/ic_mic.png Binary files differnew file mode 100644 index 000000000..13d21274a --- /dev/null +++ b/src/ui/android/res/drawable-xhdpi/ic_mic.png diff --git a/src/ui/android/res/drawable-xxhdpi/ic_launcher.png b/src/ui/android/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..da2c7a235 --- /dev/null +++ b/src/ui/android/res/drawable-xxhdpi/ic_launcher.png diff --git a/src/ui/android/res/drawable/brushed_metal.png b/src/ui/android/res/drawable/brushed_metal.png Binary files differdeleted file mode 100644 index c2f03fe7d..000000000 --- a/src/ui/android/res/drawable/brushed_metal.png +++ /dev/null diff --git a/src/ui/android/res/drawable/first_person_utterance_bg.xml b/src/ui/android/res/drawable/first_person_utterance_bg.xml new file mode 100644 index 000000000..9eb02aef1 --- /dev/null +++ b/src/ui/android/res/drawable/first_person_utterance_bg.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="4dp" /> + <solid android:color="#75CD75" /> +</shape>
\ No newline at end of file diff --git a/src/ui/android/res/drawable/icon.png b/src/ui/android/res/drawable/icon.png Binary files differdeleted file mode 100644 index a07c69fa5..000000000 --- a/src/ui/android/res/drawable/icon.png +++ /dev/null diff --git a/src/ui/android/res/drawable/second_person_utterance_bg.xml b/src/ui/android/res/drawable/second_person_utterance_bg.xml new file mode 100644 index 000000000..4acf07c67 --- /dev/null +++ b/src/ui/android/res/drawable/second_person_utterance_bg.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="4dp" /> + <solid android:color="#7575CD" /> +</shape>
\ No newline at end of file diff --git a/src/ui/android/res/layout/activity_main.xml b/src/ui/android/res/layout/activity_main.xml new file mode 100644 index 000000000..b0ccab0ea --- /dev/null +++ b/src/ui/android/res/layout/activity_main.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="match_parent" + android:layout_width="match_parent" + > + + <RelativeLayout + android:id="@+id/top_bg" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:layout_alignParentTop="true" + android:layout_alignParentLeft="true" + android:layout_alignParentRight="true" + android:padding="8dp" + android:background="#C0C0C0" + > + + <ImageView + android:id="@+id/start_stop" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_alignTop="@+id/source_language" + android:layout_alignBottom="@+id/target_language" + android:layout_alignParentRight="true" + android:padding="8dp" + android:src="@drawable/ic_mic" + android:background="?android:attr/selectableItemBackground" + android:contentDescription="@string/microphone" + /> + + <ImageView + android:id="@+id/switch_languages" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_alignTop="@+id/source_language" + android:layout_alignBottom="@+id/target_language" + android:layout_toLeftOf="@id/start_stop" + android:padding="8dp" + android:src="@drawable/ic_action_switch" + android:background="?android:attr/selectableItemBackground" + android:contentDescription="@string/switch_languages" + /> + + <org.grammaticalframework.ui.android.LanguageSelector + android:id="@+id/source_language" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:layout_toLeftOf="@id/switch_languages" + android:padding="0dp" + /> + + <org.grammaticalframework.ui.android.LanguageSelector + android:id="@+id/target_language" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:layout_below="@id/source_language" + android:layout_toLeftOf="@id/switch_languages" + android:padding="0dp" + /> + + </RelativeLayout> + + <org.grammaticalframework.ui.android.ConversationView + android:id="@+id/conversation" + android:layout_height="match_parent" + android:layout_width="match_parent" + android:layout_alignParentLeft="true" + android:layout_alignParentBottom="true" + android:layout_alignParentRight="true" + android:layout_below="@id/top_bg" + > + <LinearLayout + android:id="@+id/conversation_content" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:orientation="vertical" + android:padding="16dp" + > + </LinearLayout> + </org.grammaticalframework.ui.android.ConversationView> + +</RelativeLayout>
\ No newline at end of file diff --git a/src/ui/android/res/layout/first_person_utterance.xml b/src/ui/android/res/layout/first_person_utterance.xml new file mode 100644 index 000000000..55779ae8f --- /dev/null +++ b/src/ui/android/res/layout/first_person_utterance.xml @@ -0,0 +1,11 @@ +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_marginBottom="16dp" + android:layout_marginRight="32dp" + android:layout_gravity="left" + android:padding="8dp" + android:textSize="20sp" + android:background="@drawable/first_person_utterance_bg" + /> diff --git a/src/ui/android/res/layout/languages_item.xml b/src/ui/android/res/layout/languages_item.xml new file mode 100644 index 000000000..d5f47ab27 --- /dev/null +++ b/src/ui/android/res/layout/languages_item.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:padding="8dp" + android:textSize="20sp" + /> diff --git a/src/ui/android/res/layout/second_person_utterance.xml b/src/ui/android/res/layout/second_person_utterance.xml new file mode 100644 index 000000000..416d85328 --- /dev/null +++ b/src/ui/android/res/layout/second_person_utterance.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_marginBottom="16dp" + android:layout_marginLeft="32dp" + android:layout_gravity="right" + android:padding="8dp" + android:textSize="20sp" + android:background="@drawable/second_person_utterance_bg" + /> diff --git a/src/ui/android/res/values-sw600dp/dimens.xml b/src/ui/android/res/values-sw600dp/dimens.xml new file mode 100644 index 000000000..44f01db75 --- /dev/null +++ b/src/ui/android/res/values-sw600dp/dimens.xml @@ -0,0 +1,8 @@ +<resources> + + <!-- + Customize dimensions originally defined in res/values/dimens.xml (such as + screen margins) for sw600dp devices (e.g. 7" tablets) here. + --> + +</resources> diff --git a/src/ui/android/res/values-sw720dp-land/dimens.xml b/src/ui/android/res/values-sw720dp-land/dimens.xml new file mode 100644 index 000000000..61e3fa8fb --- /dev/null +++ b/src/ui/android/res/values-sw720dp-land/dimens.xml @@ -0,0 +1,9 @@ +<resources> + + <!-- + Customize dimensions originally defined in res/values/dimens.xml (such as + screen margins) for sw720dp devices (e.g. 10" tablets) in landscape here. + --> + <dimen name="activity_horizontal_margin">128dp</dimen> + +</resources> diff --git a/src/ui/android/res/values-v11/styles.xml b/src/ui/android/res/values-v11/styles.xml new file mode 100644 index 000000000..3c02242ad --- /dev/null +++ b/src/ui/android/res/values-v11/styles.xml @@ -0,0 +1,11 @@ +<resources> + + <!-- + Base application theme for API 11+. This theme completely replaces + AppBaseTheme from res/values/styles.xml on API 11+ devices. + --> + <style name="AppBaseTheme" parent="android:Theme.Holo.Light"> + <!-- API 11 theme customizations can go here. --> + </style> + +</resources> diff --git a/src/ui/android/res/values-v14/styles.xml b/src/ui/android/res/values-v14/styles.xml new file mode 100644 index 000000000..a91fd0372 --- /dev/null +++ b/src/ui/android/res/values-v14/styles.xml @@ -0,0 +1,12 @@ +<resources> + + <!-- + Base application theme for API 14+. This theme completely replaces + AppBaseTheme from BOTH res/values/styles.xml and + res/values-v11/styles.xml on API 14+ devices. + --> + <style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar"> + <!-- API 14 theme customizations can go here. --> + </style> + +</resources> diff --git a/src/ui/android/res/values/dimens.xml b/src/ui/android/res/values/dimens.xml new file mode 100644 index 000000000..55c1e5908 --- /dev/null +++ b/src/ui/android/res/values/dimens.xml @@ -0,0 +1,7 @@ +<resources> + + <!-- Default screen margins, per the Android Design guidelines. --> + <dimen name="activity_horizontal_margin">16dp</dimen> + <dimen name="activity_vertical_margin">16dp</dimen> + +</resources> diff --git a/src/ui/android/res/values/strings.xml b/src/ui/android/res/values/strings.xml index 0d7b9a606..26601be1b 100644 --- a/src/ui/android/res/values/strings.xml +++ b/src/ui/android/res/values/strings.xml @@ -1,8 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="app_name">GFTranslator</string> - <string name="action_settings">Settings</string> - <string name="hello_world">Hello world!</string> + <string name="app_name">GF Translator</string> + + <string name="microphone">Microphone</string> + <string name="switch_languages">Switch languages</string> </resources> diff --git a/src/ui/android/res/values/styles.xml b/src/ui/android/res/values/styles.xml new file mode 100644 index 000000000..6ce89c7ba --- /dev/null +++ b/src/ui/android/res/values/styles.xml @@ -0,0 +1,20 @@ +<resources> + + <!-- + Base application theme, dependent on API level. This theme is replaced + by AppBaseTheme from res/values-vXX/styles.xml on newer devices. + --> + <style name="AppBaseTheme" parent="android:Theme.Light"> + <!-- + Theme customizations available in newer API levels can go in + res/values-vXX/styles.xml, while customizations related to + backward-compatibility can go here. + --> + </style> + + <!-- Application theme. --> + <style name="AppTheme" parent="AppBaseTheme"> + <!-- All customizations that are NOT specific to a particular API-level can go here. --> + </style> + +</resources> diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/ASR.java b/src/ui/android/src/org/grammaticalframework/ui/android/ASR.java new file mode 100644 index 000000000..ef6df5198 --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/ASR.java @@ -0,0 +1,240 @@ + +package org.grammaticalframework.ui.android; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.speech.RecognitionListener; +import android.speech.RecognizerIntent; +import android.speech.SpeechRecognizer; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; + +/** + * Convenience wrapper around the {@link SpeechRecognizer} API. + */ +public class ASR { + + private static final boolean DBG = true; + private static final String TAG = "ASR"; + + private final Context mContext; + + private SpeechRecognizer mSpeechRecognizer; + + private String mLanguage = null; + + private State mState = State.IDLE; + + private Listener mListener; + + public static enum State { + IDLE, INITIALIZING, WAITING_FOR_SPEECH, RECORDING, WAITING_FOR_RESULTS; + } + + public ASR(Context context) { + mContext = context; + if (SpeechRecognizer.isRecognitionAvailable(context)) { + mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context); + mSpeechRecognizer.setRecognitionListener(new MyRecognitionListener()); + } + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public void setLanguage(String language) { + mLanguage = language; + } + + public void startRecognition() { + Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + if (!TextUtils.isEmpty(mLanguage)) { + intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, mLanguage); + } + intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); + intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 2); + intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); + // Weird, this shouldn't be required, but on ICS it seems to be + intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, + mContext.getPackageName()); + + mSpeechRecognizer.startListening(intent); + setState(State.INITIALIZING); + } + + public void stopRecognition() { + mSpeechRecognizer.stopListening(); + setState(State.IDLE); + } + + public boolean isRunning() { + return mState != State.IDLE; + } + + private void setState(State newState) { + if (DBG) Log.d(TAG, "Entering state: " + newState); + mState = newState; + if (mListener != null) { + mListener.onStateChanged(mState); + } + } + + public State getState() { + return mState; + } + + public void destroy() { + if (mSpeechRecognizer != null) { + mSpeechRecognizer.destroy(); + mSpeechRecognizer = null; + } + } + + private void handlePartialInput(String text) { + if (mListener != null) { + mListener.onPartialInput(text); + } + } + + private void handleSpeechInput(String text) { + if (mListener != null) { + mListener.onSpeechInput(text); + } + } + + private class MyRecognitionListener implements RecognitionListener { + @Override + public void onReadyForSpeech(Bundle params) { + if (DBG) Log.d(TAG, "onReadyForSpeech"); + setState(State.WAITING_FOR_SPEECH); + } + + @Override + public void onBeginningOfSpeech() { + if (DBG) Log.d(TAG, "onBeginningOfSpeech"); + setState(State.RECORDING); + } + + @Override + public void onBufferReceived(byte[] buffer) { + // Ignore + } + + @Override + public void onRmsChanged(float rmsdB) { + if (DBG) Log.d(TAG, "onRmsChanged(" + rmsdB + ")"); + } + + @Override + public void onEndOfSpeech() { + if (DBG) Log.d(TAG, "onEndOfSpeech"); + setState(State.WAITING_FOR_RESULTS); + } + + @Override + public void onError(int error) { + if (DBG) Log.d(TAG, "Error: " + errorMessage(error) + " (" + error + ")"); + setState(State.IDLE); + } + + private String errorMessage(int speechRecognizerError) { + switch(speechRecognizerError) { + case SpeechRecognizer.ERROR_NETWORK_TIMEOUT: + return "network timeout"; + case SpeechRecognizer.ERROR_NETWORK: + return "network"; + case SpeechRecognizer.ERROR_AUDIO: + return "audio"; + case SpeechRecognizer.ERROR_SERVER: + return "server"; + case SpeechRecognizer.ERROR_CLIENT: + return "client"; + case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: + return "timeout waiting for speech"; + case SpeechRecognizer.ERROR_NO_MATCH: + return "no match found"; + case SpeechRecognizer.ERROR_RECOGNIZER_BUSY: + return "recognizer busy"; + case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS: + return "insufficient permissions (missing RECORD_AUDIO?)"; + default: + return "unknown"; + } + } + + @Override + public void onEvent(int eventType, Bundle params) { + if (DBG) Log.d(TAG, "onEvent(" + eventType + ")"); + } + + @Override + public void onPartialResults(Bundle bundle) { + if (DBG) { + StringBuilder sb = new StringBuilder(); + sb.append("onPartialResults:"); + appendResults(sb, bundle); + Log.d(TAG, sb.toString()); + } + + String result = getResult(bundle); + if (!TextUtils.isEmpty(result)) { + handlePartialInput(result); + } + } + + @Override + public void onResults(Bundle bundle) { + if (DBG) { + StringBuilder sb = new StringBuilder(); + sb.append("onResults:"); + appendResults(sb, bundle); + Log.d(TAG, sb.toString()); + } + + setState(State.IDLE); + + String result = getResult(bundle); + if (!TextUtils.isEmpty(result)) { + handleSpeechInput(result); + } + } + + private String getResult(Bundle bundle) { + ArrayList<String> results = + bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + if (results != null && !results.isEmpty()) { + return results.get(0); + } else { + return null; + } + } + + private void appendResults(StringBuilder sb, Bundle bundle) { + ArrayList<String> results = + bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + float[] scores = bundle.getFloatArray(SpeechRecognizer.CONFIDENCE_SCORES); + + if (results != null) { + int size = results.size(); + for (int i = 0; i < size; i++) { + sb.append("\n> ").append(results.get(i)); + if (scores != null && i < scores.length) { + sb.append(" [").append(scores[i]).append("]"); + } + } + } + } + } + + public interface Listener { + void onPartialInput(String input); + void onSpeechInput(String input); + void onStateChanged(State newState); + } + +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/ConversationView.java b/src/ui/android/src/org/grammaticalframework/ui/android/ConversationView.java new file mode 100644 index 000000000..3923d13a0 --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/ConversationView.java @@ -0,0 +1,62 @@ +package org.grammaticalframework.ui.android; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.ScrollView; +import android.widget.TextView; + +public class ConversationView extends ScrollView { + + private LayoutInflater mInflater; + + private ViewGroup mContent; + + public ConversationView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public ConversationView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ConversationView(Context context) { + super(context); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mContent = (ViewGroup) findViewById(R.id.conversation_content); + mInflater = LayoutInflater.from(getContext()); + } + + public void addFirstPersonUtterance(CharSequence text) { + addUtterance(R.layout.first_person_utterance, text); + } + + public void addSecondPersonUtterance(CharSequence text) { + addUtterance(R.layout.second_person_utterance, text); + } + + private void addUtterance(int res, CharSequence text) { + TextView view = (TextView) mInflater.inflate(res, mContent, false); + view.setText(text); + mContent.addView(view); + post(new Runnable() { + public void run() { + fullScroll(FOCUS_DOWN); + } + }); + } + + public void updateLastUtterance(CharSequence text) { + int count = mContent.getChildCount(); + if (count > 0) { + TextView view = (TextView) mContent.getChildAt(count - 1); + view.setText(text); + } + } + +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/Language.java b/src/ui/android/src/org/grammaticalframework/ui/android/Language.java new file mode 100644 index 000000000..8adc74609 --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/Language.java @@ -0,0 +1,31 @@ +package org.grammaticalframework.ui.android; + +public class Language { + private final String mLangCode; + private final String mLangName; + private final String mConcrete; + + public Language(String langCode, String langName, String concrete) { + mLangCode = langCode; + mLangName = langName; + mConcrete = concrete; + } + + public String getLangCode() { + return mLangCode; + } + + public String getLangName() { + return mLangName; + } + + String getConcrete() { + return mConcrete; + } + + @Override + public String toString() { + return getLangName(); + } + +}
\ No newline at end of file diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/LanguageSelector.java b/src/ui/android/src/org/grammaticalframework/ui/android/LanguageSelector.java new file mode 100644 index 000000000..d3148cda4 --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/LanguageSelector.java @@ -0,0 +1,51 @@ +package org.grammaticalframework.ui.android; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.AdapterView; +import android.widget.Spinner; + +import java.util.List; + +public class LanguageSelector extends Spinner { + + public LanguageSelector(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public LanguageSelector(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LanguageSelector(Context context) { + super(context); + } + + public void setLanguages(List<Language> languages) { + setAdapter(new LanguagesAdapter(getContext(), languages)); + } + + public void setSelectedLanguage(Language selected) { + setSelection(((LanguagesAdapter) getAdapter()).getPosition(selected)); + } + + public void setOnLanguageSelectedListener(final OnLanguageSelectedListener listener) { + setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + if (listener != null) { + listener.onLanguageSelected((Language) parent.getItemAtPosition(position)); + } + } + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + }); + } + + public interface OnLanguageSelectedListener { + void onLanguageSelected(Language language); + } + +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/LanguagesAdapter.java b/src/ui/android/src/org/grammaticalframework/ui/android/LanguagesAdapter.java new file mode 100644 index 000000000..e39ed7bd9 --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/LanguagesAdapter.java @@ -0,0 +1,16 @@ +package org.grammaticalframework.ui.android; + +import android.content.Context; +import android.widget.ArrayAdapter; +import android.widget.SpinnerAdapter; + + +import java.util.List; + +public class LanguagesAdapter extends ArrayAdapter<Language> implements SpinnerAdapter { + + public LanguagesAdapter(Context context, List<Language> objects) { + super(context, R.layout.languages_item, objects); + } + +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/LocaleUtils.java b/src/ui/android/src/org/grammaticalframework/ui/android/LocaleUtils.java new file mode 100644 index 000000000..9fb048908 --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/LocaleUtils.java @@ -0,0 +1,42 @@ +package org.grammaticalframework.ui.android; + +import android.text.TextUtils; + +import java.util.Locale; + +/** + * Collections of utils to handle locales. + */ +public class LocaleUtils { + + /** + * Parses a locale string formatted by {@link Locale#toString()}. + * + * @return the parsed {@code Locale} or {@code defaultLocale} if the input was null or empty. + */ + public static Locale parseJavaLocale(String localeString, Locale defaultLocale) { + if (TextUtils.isEmpty(localeString)) { + return defaultLocale; + } + final char separator = '_'; + int pos1 = localeString.indexOf(separator); + if (pos1 == -1) { + return new Locale(localeString); + } + String language = localeString.substring(0, pos1); + + int start2 = pos1 + 1; + int pos2 = localeString.indexOf(separator, start2); + if (pos2 == -1) { + return new Locale(language, localeString.substring(start2)); + } + String country = localeString.substring(start2, pos2); + + int start3 = pos2 + 1; + int pos3 = localeString.indexOf(separator, start3); + String variant = (pos3 == -1) + ? localeString.substring(start3) + : localeString.substring(start3, pos3); + return new Locale(language, country, variant); + } +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/MainActivity.java b/src/ui/android/src/org/grammaticalframework/ui/android/MainActivity.java new file mode 100644 index 000000000..fba9987d6 --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/MainActivity.java @@ -0,0 +1,202 @@ + +package org.grammaticalframework.ui.android; + +import android.app.Activity; +import android.os.AsyncTask; +import android.os.Bundle; +import android.speech.SpeechRecognizer; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageView; + +import org.grammaticalframework.ui.android.ASR.State; +import org.grammaticalframework.ui.android.LanguageSelector.OnLanguageSelectedListener; + +public class MainActivity extends Activity { + + private static final boolean DBG = true; + private static final String TAG = "DemoActivity"; + + private static final boolean FAKE_SPEECH = false; + + private ImageView mStartStopButton; + + private ConversationView mConversationView; + + private LanguageSelector mSourceLanguageView; + + private LanguageSelector mTargetLanguageView; + + private ImageView mSwitchLanguagesButton; + + private ASR mAsr; + + private TTS mTts; + + private Translator mTranslator; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mStartStopButton = (ImageView) findViewById(R.id.start_stop); + mConversationView = (ConversationView) findViewById(R.id.conversation); + mSourceLanguageView = (LanguageSelector) findViewById(R.id.source_language); + mTargetLanguageView = (LanguageSelector) findViewById(R.id.target_language); + mSwitchLanguagesButton = (ImageView) findViewById(R.id.switch_languages); + + mStartStopButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mAsr.isRunning()) { + stopRecognition(); + } else { + startRecognition(); + } + } + }); + + mStartStopButton.setEnabled(SpeechRecognizer.isRecognitionAvailable(this)); + + mAsr = new ASR(this); + mAsr.setListener(new SpeechInputListener()); + + mTts = new TTS(this); + + mTranslator = new Translator(this); + new AsyncTask<Void,Void,Void>() { + @Override + protected Void doInBackground(Void... params) { + mTranslator.init(); + return null; + } + }.execute(); + + mSourceLanguageView.setLanguages(mTranslator.getAvailableSourceLanguages()); + mSourceLanguageView.setSelectedLanguage(mTranslator.getSourceLanguage()); + mSourceLanguageView.setOnLanguageSelectedListener(new OnLanguageSelectedListener() { + @Override + public void onLanguageSelected(Language language) { + onSourceLanguageSelected(language); + } + }); + mTargetLanguageView.setLanguages(mTranslator.getAvailableTargetLanguages()); + mTargetLanguageView.setSelectedLanguage(mTranslator.getTargetLanguage()); + mTargetLanguageView.setOnLanguageSelectedListener(new OnLanguageSelectedListener() { + @Override + public void onLanguageSelected(Language language) { + onTargetLanguageSelected(language); + } + }); + + mSwitchLanguagesButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onSwitchLanguages(); + } + }); + } + + @Override + protected void onDestroy() { + if (mAsr != null) { + mAsr.destroy(); + mAsr = null; + } + if (mTts != null) { + mTts.destroy(); + mTts = null; + } + super.onDestroy(); + } + + void onSourceLanguageSelected(Language language) { + mTranslator.setSourceLanguage(language); + } + + void onTargetLanguageSelected(Language language) { + mTranslator.setTargetLanguage(language); + } + + public String getSourceLanguageCode() { + return mTranslator.getSourceLanguage().getLangCode(); + } + + public String getTargetLanguageCode() { + return mTranslator.getTargetLanguage().getLangCode(); + } + + void onSwitchLanguages() { + Language newSource = mTranslator.getTargetLanguage(); + Language newTarget = mTranslator.getSourceLanguage(); + mSourceLanguageView.setSelectedLanguage(newSource); + mTargetLanguageView.setSelectedLanguage(newTarget); + } + + private void startRecognition() { + mConversationView.addFirstPersonUtterance("..."); + + if (FAKE_SPEECH) { + handleSpeechInput("where is the hotel"); + } else { + mAsr.setLanguage(getSourceLanguageCode()); + mAsr.startRecognition(); + } + } + + private void stopRecognition() { + mAsr.stopRecognition(); + } + + private void handlePartialSpeechInput(String input) { + mConversationView.updateLastUtterance(input); + } + + private void handleSpeechInput(final String input) { + mConversationView.updateLastUtterance(input); + new AsyncTask<Void,Void,String>() { + @Override + protected String doInBackground(Void... params) { + return mTranslator.translate(input); + } + + @Override + protected void onPostExecute(String result) { + outputText(result); + } + }.execute(); + } + + private void outputText(String text) { + if (DBG) Log.d(TAG, "Speaking: " + text); + mConversationView.addSecondPersonUtterance(text); + if (!FAKE_SPEECH) { + mTts.setLanguage(getTargetLanguageCode()); + mTts.speak(text); + } + } + + private class SpeechInputListener implements ASR.Listener { + + @Override + public void onPartialInput(String input) { + handlePartialSpeechInput(input); + } + + @Override + public void onSpeechInput(String input) { + handleSpeechInput(input); + } + + @Override + public void onStateChanged(State newState) { +// if (newState == ASR.State.IDLE) { +// mStartStopButton.setImageResource(R.drawable.mic_idle); +// } else { +// mStartStopButton.setImageResource(R.drawable.mic_open); +// } + } + } +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/TTS.java b/src/ui/android/src/org/grammaticalframework/ui/android/TTS.java new file mode 100644 index 000000000..6993a3fc6 --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/TTS.java @@ -0,0 +1,63 @@ +package org.grammaticalframework.ui.android; + +import android.content.Context; +import android.speech.tts.TextToSpeech; +import android.util.Log; + +import java.util.HashMap; +import java.util.Locale; + +public class TTS { + + private static final String TAG = "TTS"; + + private TextToSpeech mTts; + + public TTS(Context context) { + mTts = new TextToSpeech(context, new InitListener()); + } + + public void setLanguage(String language) { + Locale locale = LocaleUtils.parseJavaLocale(language.replace('-', '_'), + Locale.getDefault()); + + int result = mTts.setLanguage(locale); + if (result == TextToSpeech.LANG_MISSING_DATA || + result == TextToSpeech.LANG_NOT_SUPPORTED) { + Log.e(TAG, "Language is not available"); + } else { + // TODO: the language may be available for the locale, + // but not for the specified country and variant. + } + } + + // TODO: handle speak() calls before service connects + public void speak(String text) { + HashMap<String,String> params = new HashMap<String,String>(); + // TODO: how can I get network / embedded fallback? + // Using both crashes the TTS engine if the offline data is not installed + // Using only one doesn't allow the other +// params.put(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS, "true"); +// params.put(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS, "true"); + mTts.speak(text, TextToSpeech.QUEUE_FLUSH, params); + } + + public void destroy() { + if (mTts != null) { + mTts.stop(); + mTts.shutdown(); + } + } + + private class InitListener implements TextToSpeech.OnInitListener { + @Override + public void onInit(int status) { + if (status == TextToSpeech.SUCCESS) { + Log.d(TAG, "Initialized TTS"); + } else { + Log.e(TAG, "Failed to initialize TTS"); + } + } + + } +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java b/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java new file mode 100644 index 000000000..9ecdb104e --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java @@ -0,0 +1,144 @@ +package org.grammaticalframework.ui.android; + +import android.content.Context; +import android.util.Log; + +import org.grammaticalframework.pgf.Concr; +import org.grammaticalframework.pgf.Expr; +import org.grammaticalframework.pgf.PGF; +import org.grammaticalframework.pgf.ParseError; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; + +public class Translator { + + private static final String TAG = "Translator"; + + // TODO: allow changing + private String mGrammar = "ResourceDemo.pgf"; + + // TODO: build dynamically? + private Language[] mLanguages = { + new Language("en-US", "English", "ResourceDemoEng"), + new Language("de-DE", "German", "ResourceDemoGer"), + new Language("es-ES", "Spanish", "ResourceDemoSpa"), + new Language("fr-FR", "French", "ResourceDemoFre"), + }; + + private final Context mContext; + + private Language mSourceLanguage; + + private Language mTargetLanguage; + + private PGF mPgf; + + public Translator(Context context) { + mContext = context; + } + + public Context getContext() { + return mContext; + } + + public List<Language> getAvailableSourceLanguages() { + return Arrays.asList(mLanguages); + } + + public List<Language> getAvailableTargetLanguages() { + return Arrays.asList(mLanguages); + } + + public void setSourceLanguage(Language language) { + mSourceLanguage = language; + } + + public void setTargetLanguage(Language language) { + mTargetLanguage = language; + } + + public Language getSourceLanguage() { + return mSourceLanguage != null ? mSourceLanguage : mLanguages[0]; + } + + public Language getTargetLanguage() { + return mTargetLanguage != null ? mTargetLanguage : mLanguages[1]; + } + + /** + * Takes a lot of time. Must not be called on the main thread. + */ + public void init() { + ensureLoaded(mGrammar); + } + + /** + * Takes a lot of time. Must not be called on the main thread. + */ + public String translate(String input) { + ensureLoaded(mGrammar); + return translateInternal(input); + } + + private synchronized void ensureLoaded(String grammarName) { + if (mPgf != null) return; + + try { + // TODO: use PGF API to read this directly from assets + Log.d(TAG, "Copying grammar..."); + File file = copyAsset(grammarName); + Log.d(TAG, "Trying to open " + file); + mPgf = PGF.readPGF(file.getPath()); + } catch (FileNotFoundException e) { + Log.e(TAG, "File not found", e); + } catch (IOException e) { + Log.e(TAG, "Error loading grammar", e); + } + } + + private File copyAsset(String asset) throws IOException { + InputStream in = null; + OutputStream out = null; + try { + in = getContext().getAssets().open(asset); + out = getContext().openFileOutput(asset, Context.MODE_PRIVATE); + byte[] buf = new byte[4096]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + return getContext().getFileStreamPath(asset); + } finally { + if (in != null) { + in.close(); + } + if (out != null) { + out.close(); + } + } + } + + protected String translateInternal(String input) { + try { + Concr sourceGrammar = getConcr(getSourceLanguage().getConcrete()); + Expr expr = sourceGrammar.parseBest("S", input); + Concr targetGrammar = getConcr(getTargetLanguage().getConcrete()); + String output = targetGrammar.linearize(expr); + return output; + } catch (ParseError e) { + Log.e(TAG, "Parse error: " + e); + return "parse error"; // TODO: no no no + } + } + + private Concr getConcr(String name) { + return mPgf.getLanguages().get(name); + } + +} diff --git a/src/ui/android/src/se/fnord/android/layout/PredicateLayout.java b/src/ui/android/src/se/fnord/android/layout/PredicateLayout.java deleted file mode 100644 index 4734d4618..000000000 --- a/src/ui/android/src/se/fnord/android/layout/PredicateLayout.java +++ /dev/null @@ -1,134 +0,0 @@ -package se.fnord.android.layout;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * ViewGroup that arranges child views in a similar way to text, with them laid
- * out one line at a time and "wrapping" to the next line as needed.
- *
- * Code licensed under CC-by-SA
- *
- * @author Henrik Gustafsson
- * @see http://stackoverflow.com/questions/549451/line-breaking-widget-layout-for-android
- * @license http://creativecommons.org/licenses/by-sa/2.5/
- *
- */
-public class PredicateLayout extends ViewGroup {
-
- private int line_height;
-
- public static class LayoutParams extends ViewGroup.LayoutParams {
- public final int horizontal_spacing;
- public final int vertical_spacing;
-
- /**
- * @param horizontal_spacing Pixels between items, horizontally
- * @param vertical_spacing Pixels between items, vertically
- */
- public LayoutParams(int horizontal_spacing, int vertical_spacing) {
- this(0, 0, horizontal_spacing, vertical_spacing);
- }
-
- /**
- * @param width
- * @param height
- * @param horizontal_spacing Pixels between items, horizontally
- * @param vertical_spacing Pixels between items, vertically
- */
- public LayoutParams(int width, int height, int horizontal_spacing, int vertical_spacing) {
- super(width, height);
- this.horizontal_spacing = horizontal_spacing;
- this.vertical_spacing = vertical_spacing;
- }
- }
-
- public PredicateLayout(Context context) {
- super(context);
- }
-
- public PredicateLayout(Context context, AttributeSet attrs){
- super(context, attrs);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- assert(MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED);
-
- final int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
- int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
- final int count = getChildCount();
- int line_height = 0;
-
- int xpos = getPaddingLeft();
- int ypos = getPaddingTop();
-
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() != GONE) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- child.measure(
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
- MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
-
- final int childw = child.getMeasuredWidth();
- line_height = Math.max(line_height, child.getMeasuredHeight() + lp.vertical_spacing);
-
- if (xpos + childw > width) {
- xpos = getPaddingLeft();
- ypos += line_height;
- }
-
- xpos += childw + lp.horizontal_spacing;
- }
- }
- this.line_height = line_height;
-
- if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED){
- height = ypos + line_height;
-
- } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST){
- if (ypos + line_height < height){
- height = ypos + line_height;
- }
- }
- setMeasuredDimension(width, height);
- }
-
- @Override
- protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
- return new LayoutParams(1, 1); // default of 1px spacing
- }
-
- @Override
- protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
- if (p instanceof LayoutParams)
- return true;
- return false;
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- final int count = getChildCount();
- final int width = r - l;
- int xpos = getPaddingLeft();
- int ypos = getPaddingTop();
-
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() != GONE) {
- final int childw = child.getMeasuredWidth();
- final int childh = child.getMeasuredHeight();
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- if (xpos + childw > width) {
- xpos = getPaddingLeft();
- ypos += line_height;
- }
- child.layout(xpos, ypos, xpos + childw, ypos + childh);
- xpos += childw + lp.horizontal_spacing;
- }
- }
- }
-}
\ No newline at end of file |
