Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions circle.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions example/build.gradle
Original file line number Diff line number Diff line change
@@ -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")
}

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ContactsCompletionViewTest {

@Rule
public ActivityTestRule<TokenActivity> activityRule = new ActivityTestRule<>(
TokenActivity.class);

@Test
public void completesOnComma() throws Exception {
onView(withId(R.id.searchView))
.perform(typeText("mar,"))
.check(matches(emailForPerson(2, is("[email protected]"))));
}

@Test
public void doesntCompleteWithoutComma() throws Exception {
onView(withId(R.id.searchView))
.perform(typeText("mar"))
.check(matches(tokenCount(is(2))));
}
}
Original file line number Diff line number Diff line change
@@ -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<View> emailForPerson(final int position, final Matcher<String> stringMatcher) {
checkNotNull(stringMatcher);
return new BoundedMatcher<View, ContactsCompletionView>(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<View> tokenCount(final Matcher<Integer> intMatcher) {
checkNotNull(intMatcher);
return new BoundedMatcher<View, ContactsCompletionView>(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());
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ViewSpanTest {

@Rule
public ActivityTestRule<TokenActivity> 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);
}
}
3 changes: 2 additions & 1 deletion example/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
52 changes: 29 additions & 23 deletions library/src/main/java/com/tokenautocomplete/ViewSpan.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down