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();