diff --git a/circle.yml b/circle.yml
index 22dde61c..54962c8f 100644
--- a/circle.yml
+++ b/circle.yml
@@ -1,12 +1,21 @@
-checkout:
- post:
- - cp -r .circleci/licenses/. $ANDROID_HOME/licenses
-
machine:
java:
version:
oraclejdk8
+dependencies:
+ pre:
+ # Android SDK Build-tools, revision 25.0.3, makes sure we've accepted the license
+ - if [ ! -d "/usr/local/android-sdk-linux/build-tools/25.0.3" ]; then echo y | android update sdk --no-ui --all --filter "build-tools-25.0.3"; fi
+ cache_directories:
+ - /usr/local/android-sdk-linux/build-tools/25.0.3
+ post:
+ - emulator -avd circleci-android22 -no-window:
+ background: true
+ parallel: true
+
test:
override:
- ./gradlew test
+ - circle-android wait-for-boot
+ - ./gradlew connectedAndroidTest
diff --git a/example/build.gradle b/example/build.gradle
index 2ed25b14..7aa2952e 100644
--- a/example/build.gradle
+++ b/example/build.gradle
@@ -1,6 +1,10 @@
apply plugin: 'com.android.application'
dependencies {
+ androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+
compile project(":library")
}
@@ -13,6 +17,8 @@ android {
targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION)
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
diff --git a/example/src/androidTest/java/com/tokenautocomplete/ContactsCompletionViewTest.java b/example/src/androidTest/java/com/tokenautocomplete/ContactsCompletionViewTest.java
new file mode 100644
index 00000000..e9b536d2
--- /dev/null
+++ b/example/src/androidTest/java/com/tokenautocomplete/ContactsCompletionViewTest.java
@@ -0,0 +1,44 @@
+package com.tokenautocomplete;
+
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.typeText;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
+import static com.tokenautocomplete.TokenMatchers.emailForPerson;
+import static com.tokenautocomplete.TokenMatchers.tokenCount;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ContactsCompletionViewTest {
+
+ @Rule
+ public ActivityTestRule activityRule = new ActivityTestRule<>(
+ TokenActivity.class);
+
+ @Test
+ public void completesOnComma() throws Exception {
+ onView(withId(R.id.searchView))
+ .perform(typeText("mar,"))
+ .check(matches(emailForPerson(2, is("marshall@example.com"))));
+ }
+
+ @Test
+ public void doesntCompleteWithoutComma() throws Exception {
+ onView(withId(R.id.searchView))
+ .perform(typeText("mar"))
+ .check(matches(tokenCount(is(2))));
+ }
+}
\ No newline at end of file
diff --git a/example/src/androidTest/java/com/tokenautocomplete/TokenMatchers.java b/example/src/androidTest/java/com/tokenautocomplete/TokenMatchers.java
new file mode 100644
index 00000000..26674cab
--- /dev/null
+++ b/example/src/androidTest/java/com/tokenautocomplete/TokenMatchers.java
@@ -0,0 +1,48 @@
+package com.tokenautocomplete;
+
+import android.support.test.espresso.matcher.BoundedMatcher;
+import android.view.View;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+import java.util.Locale;
+
+import static android.support.test.espresso.core.deps.guava.base.Preconditions.checkNotNull;
+
+/** Convenience matchers to make it easier to check token view contents
+ * Created by mgod on 8/25/17.
+ */
+
+class TokenMatchers {
+ static Matcher emailForPerson(final int position, final Matcher stringMatcher) {
+ checkNotNull(stringMatcher);
+ return new BoundedMatcher(ContactsCompletionView.class) {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText(String.format(Locale.US, "email for person %d: ", position));
+ stringMatcher.describeTo(description);
+ }
+ @Override
+ public boolean matchesSafely(ContactsCompletionView view) {
+ if (view.getObjects().size() <= position) { return stringMatcher.matches(null); }
+ return stringMatcher.matches(view.getObjects().get(position).getEmail());
+ }
+ };
+ }
+
+ static Matcher tokenCount(final Matcher intMatcher) {
+ checkNotNull(intMatcher);
+ return new BoundedMatcher(ContactsCompletionView.class) {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("token count: ");
+ intMatcher.describeTo(description);
+ }
+ @Override
+ public boolean matchesSafely(ContactsCompletionView view) {
+ return intMatcher.matches(view.getObjects().size());
+ }
+ };
+ }
+}
diff --git a/example/src/androidTest/java/com/tokenautocomplete/ViewSpanTest.java b/example/src/androidTest/java/com/tokenautocomplete/ViewSpanTest.java
new file mode 100644
index 00000000..76ed6c4a
--- /dev/null
+++ b/example/src/androidTest/java/com/tokenautocomplete/ViewSpanTest.java
@@ -0,0 +1,66 @@
+package com.tokenautocomplete;
+
+import android.content.Context;
+import android.graphics.Paint;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ViewSpanTest {
+
+ @Rule
+ public ActivityTestRule activityRule = new ActivityTestRule<>(
+ TokenActivity.class);
+
+ @Test
+ public void correctLineHeightWithBaseline() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ TextView textView = new TextView(appContext);
+ textView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ textView.setText("A person's name");
+
+ ViewSpan span = new ViewSpan(textView, 100);
+ Paint paint = new Paint();
+ Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
+ int width = span.getSize(paint, "", 0, 0, fontMetricsInt);
+ assertEquals(width, textView.getRight());
+ assertEquals(textView.getHeight() - textView.getBaseline(), fontMetricsInt.bottom);
+ assertEquals(-textView.getBaseline(), fontMetricsInt.top);
+ }
+
+ @Test
+ public void correctLineHeightWithoutBaseline() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ View view = new View(appContext);
+ view.setMinimumHeight(1000);
+ view.setMinimumWidth(1000);
+
+ ViewSpan span = new ViewSpan(view, 100);
+ Paint paint = new Paint();
+ Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
+ int width = span.getSize(paint, "", 0, 0, fontMetricsInt);
+ assertEquals(width, 100);
+ assertEquals(0, fontMetricsInt.bottom);
+ assertEquals(-view.getHeight(), fontMetricsInt.top);
+ }
+}
\ No newline at end of file
diff --git a/example/src/main/res/layout/activity_main.xml b/example/src/main/res/layout/activity_main.xml
index 042d5330..4b59dfbf 100644
--- a/example/src/main/res/layout/activity_main.xml
+++ b/example/src/main/res/layout/activity_main.xml
@@ -11,7 +11,8 @@
android:hint="@string/email_prompt"
android:imeOptions="actionDone"
android:textColor="@android:color/darker_gray"
- android:textSize="19sp"
+ android:textSize="16sp"
+ android:lineSpacingExtra="1dp"
android:nextFocusDown="@+id/editText"
android:inputType="text|textNoSuggestions|textMultiLine"
android:focusableInTouchMode="true" />
diff --git a/library/src/main/java/com/tokenautocomplete/ViewSpan.java b/library/src/main/java/com/tokenautocomplete/ViewSpan.java
index 0828b7b6..6280e335 100644
--- a/library/src/main/java/com/tokenautocomplete/ViewSpan.java
+++ b/library/src/main/java/com/tokenautocomplete/ViewSpan.java
@@ -2,7 +2,9 @@
import android.graphics.Canvas;
import android.graphics.Paint;
+import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.text.style.ReplacementSpan;
import android.view.View;
import android.view.ViewGroup;
@@ -16,50 +18,54 @@
public class ViewSpan extends ReplacementSpan {
protected View view;
private int maxWidth;
+ private boolean prepared;
- public ViewSpan(View v, int maxWidth) {
+ public ViewSpan(View view, int maxWidth) {
super();
this.maxWidth = maxWidth;
- view = v;
- view.setLayoutParams(new ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ this.view = view;
+ this.view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
}
private void prepView() {
- int widthSpec = View.MeasureSpec.makeMeasureSpec(maxWidth, View.MeasureSpec.AT_MOST);
- int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ if (!prepared) {
+ int widthSpec = View.MeasureSpec.makeMeasureSpec(maxWidth, View.MeasureSpec.AT_MOST);
+ int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
- view.measure(widthSpec, heightSpec);
- view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
+ view.measure(widthSpec, heightSpec);
+ view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
+ prepared = true;
+ }
}
- public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end,
- float x, int top, int y, int bottom, @NonNull Paint paint) {
+ @Override
+ public void draw(@NonNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
prepView();
canvas.save();
- //Centering the token looks like a better strategy that aligning the bottom
- int padding = (bottom - top - view.getBottom()) / 2;
- canvas.translate(x, bottom - view.getBottom() - padding);
+ canvas.translate(x, top);
view.draw(canvas);
canvas.restore();
}
- public int getSize(@NonNull Paint paint, CharSequence charSequence, int i, int i2, Paint.FontMetricsInt fm) {
+ @Override
+ public int getSize(@NonNull Paint paint, CharSequence charSequence, @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fontMetricsInt) {
prepView();
- if (fm != null) {
+ if (fontMetricsInt != null) {
//We need to make sure the layout allots enough space for the view
int height = view.getMeasuredHeight();
- int need = height - (fm.descent - fm.ascent);
- if (need > 0) {
- int ascent = need / 2;
- //This makes sure the text drawing area will be tall enough for the view
- fm.descent += need - ascent;
- fm.ascent -= ascent;
- fm.bottom += need - ascent;
- fm.top -= need / 2;
+
+ int adjustedBaseline = view.getBaseline();
+ //-1 means the view doesn't support baseline alignment, so align bottom to font baseline
+ if (adjustedBaseline == -1) {
+ adjustedBaseline = height;
}
+ fontMetricsInt.ascent = fontMetricsInt.top = -adjustedBaseline;
+ fontMetricsInt.descent = fontMetricsInt.bottom = height - adjustedBaseline;
}
return view.getRight();