Skip to content

Commit 07ee5e0

Browse files
committed
Close #245. Make 1st token line height conistent
Also adds support for Espresso tests.
1 parent 6bc7cd4 commit 07ee5e0

File tree

6 files changed

+195
-24
lines changed

6 files changed

+195
-24
lines changed

example/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
apply plugin: 'com.android.application'
22

33
dependencies {
4+
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
5+
exclude group: 'com.android.support', module: 'support-annotations'
6+
})
7+
48
compile project(":library")
59
}
610

@@ -13,6 +17,8 @@ android {
1317
targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION)
1418
versionCode Integer.parseInt(project.VERSION_CODE)
1519
versionName project.VERSION_NAME
20+
21+
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
1622
}
1723
buildTypes {
1824
release {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.tokenautocomplete;
2+
3+
import android.support.test.rule.ActivityTestRule;
4+
import android.support.test.runner.AndroidJUnit4;
5+
6+
import org.junit.Rule;
7+
import org.junit.Test;
8+
import org.junit.runner.RunWith;
9+
10+
import static android.support.test.espresso.Espresso.onView;
11+
import static android.support.test.espresso.action.ViewActions.typeText;
12+
import static android.support.test.espresso.assertion.ViewAssertions.matches;
13+
import static android.support.test.espresso.matcher.ViewMatchers.withId;
14+
15+
import static com.tokenautocomplete.TokenMatchers.emailForPerson;
16+
import static com.tokenautocomplete.TokenMatchers.tokenCount;
17+
import static org.hamcrest.Matchers.is;
18+
19+
/**
20+
* Instrumentation test, which will execute on an Android device.
21+
*
22+
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
23+
*/
24+
@RunWith(AndroidJUnit4.class)
25+
public class ContactsCompletionViewTest {
26+
27+
@Rule
28+
public ActivityTestRule<TokenActivity> activityRule = new ActivityTestRule<>(
29+
TokenActivity.class);
30+
31+
@Test
32+
public void completesOnComma() throws Exception {
33+
onView(withId(R.id.searchView))
34+
.perform(typeText("mar,"))
35+
.check(matches(emailForPerson(2, is("marshall@example.com"))));
36+
}
37+
38+
@Test
39+
public void doesntCompleteWithoutComma() throws Exception {
40+
onView(withId(R.id.searchView))
41+
.perform(typeText("mar"))
42+
.check(matches(tokenCount(is(2))));
43+
}
44+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.tokenautocomplete;
2+
3+
import android.support.test.espresso.matcher.BoundedMatcher;
4+
import android.view.View;
5+
6+
import org.hamcrest.Description;
7+
import org.hamcrest.Matcher;
8+
9+
import java.util.Locale;
10+
11+
import static android.support.test.espresso.core.deps.guava.base.Preconditions.checkNotNull;
12+
13+
/** Convenience matchers to make it easier to check token view contents
14+
* Created by mgod on 8/25/17.
15+
*/
16+
17+
class TokenMatchers {
18+
static Matcher<View> emailForPerson(final int position, final Matcher<String> stringMatcher) {
19+
checkNotNull(stringMatcher);
20+
return new BoundedMatcher<View, ContactsCompletionView>(ContactsCompletionView.class) {
21+
@Override
22+
public void describeTo(Description description) {
23+
description.appendText(String.format(Locale.US, "email for person %d: ", position));
24+
stringMatcher.describeTo(description);
25+
}
26+
@Override
27+
public boolean matchesSafely(ContactsCompletionView view) {
28+
if (view.getObjects().size() <= position) { return stringMatcher.matches(null); }
29+
return stringMatcher.matches(view.getObjects().get(position).getEmail());
30+
}
31+
};
32+
}
33+
34+
static Matcher<View> tokenCount(final Matcher<Integer> intMatcher) {
35+
checkNotNull(intMatcher);
36+
return new BoundedMatcher<View, ContactsCompletionView>(ContactsCompletionView.class) {
37+
@Override
38+
public void describeTo(Description description) {
39+
description.appendText("token count: ");
40+
intMatcher.describeTo(description);
41+
}
42+
@Override
43+
public boolean matchesSafely(ContactsCompletionView view) {
44+
return intMatcher.matches(view.getObjects().size());
45+
}
46+
};
47+
}
48+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.tokenautocomplete;
2+
3+
import android.content.Context;
4+
import android.graphics.Paint;
5+
import android.support.test.InstrumentationRegistry;
6+
import android.support.test.rule.ActivityTestRule;
7+
import android.support.test.runner.AndroidJUnit4;
8+
import android.view.View;
9+
import android.view.ViewGroup;
10+
import android.widget.TextView;
11+
12+
import org.junit.Rule;
13+
import org.junit.Test;
14+
import org.junit.runner.RunWith;
15+
16+
import static org.junit.Assert.assertEquals;
17+
18+
/**
19+
* Instrumentation test, which will execute on an Android device.
20+
*
21+
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
22+
*/
23+
@RunWith(AndroidJUnit4.class)
24+
public class ViewSpanTest {
25+
26+
@Rule
27+
public ActivityTestRule<TokenActivity> activityRule = new ActivityTestRule<>(
28+
TokenActivity.class);
29+
30+
@Test
31+
public void correctLineHeightWithBaseline() throws Exception {
32+
// Context of the app under test.
33+
Context appContext = InstrumentationRegistry.getTargetContext();
34+
35+
TextView textView = new TextView(appContext);
36+
textView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
37+
ViewGroup.LayoutParams.WRAP_CONTENT));
38+
textView.setText("A person's name");
39+
40+
ViewSpan span = new ViewSpan(textView, 100);
41+
Paint paint = new Paint();
42+
Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
43+
int width = span.getSize(paint, "", 0, 0, fontMetricsInt);
44+
assertEquals(width, textView.getRight());
45+
assertEquals(textView.getHeight() - textView.getBaseline(), fontMetricsInt.bottom);
46+
assertEquals(-textView.getBaseline(), fontMetricsInt.top);
47+
}
48+
49+
@Test
50+
public void correctLineHeightWithoutBaseline() throws Exception {
51+
// Context of the app under test.
52+
Context appContext = InstrumentationRegistry.getTargetContext();
53+
54+
View view = new View(appContext);
55+
view.setMinimumHeight(1000);
56+
view.setMinimumWidth(1000);
57+
58+
ViewSpan span = new ViewSpan(view, 100);
59+
Paint paint = new Paint();
60+
Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
61+
int width = span.getSize(paint, "", 0, 0, fontMetricsInt);
62+
assertEquals(width, 100);
63+
assertEquals(0, fontMetricsInt.bottom);
64+
assertEquals(-view.getHeight(), fontMetricsInt.top);
65+
}
66+
}

example/src/main/res/layout/activity_main.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
android:hint="@string/email_prompt"
1212
android:imeOptions="actionDone"
1313
android:textColor="@android:color/darker_gray"
14-
android:textSize="19sp"
14+
android:textSize="16sp"
15+
android:lineSpacingExtra="1dp"
1516
android:nextFocusDown="@+id/editText"
1617
android:inputType="text|textNoSuggestions|textMultiLine"
1718
android:focusableInTouchMode="true" />

library/src/main/java/com/tokenautocomplete/ViewSpan.java

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import android.graphics.Canvas;
44
import android.graphics.Paint;
5+
import android.support.annotation.IntRange;
56
import android.support.annotation.NonNull;
7+
import android.support.annotation.Nullable;
68
import android.text.style.ReplacementSpan;
79
import android.view.View;
810
import android.view.ViewGroup;
@@ -16,50 +18,54 @@
1618
public class ViewSpan extends ReplacementSpan {
1719
protected View view;
1820
private int maxWidth;
21+
private boolean prepared;
1922

20-
public ViewSpan(View v, int maxWidth) {
23+
public ViewSpan(View view, int maxWidth) {
2124
super();
2225
this.maxWidth = maxWidth;
23-
view = v;
24-
view.setLayoutParams(new ViewGroup.LayoutParams(
25-
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
26+
this.view = view;
27+
this.view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
28+
ViewGroup.LayoutParams.WRAP_CONTENT));
2629
}
2730

2831
private void prepView() {
29-
int widthSpec = View.MeasureSpec.makeMeasureSpec(maxWidth, View.MeasureSpec.AT_MOST);
30-
int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
32+
if (!prepared) {
33+
int widthSpec = View.MeasureSpec.makeMeasureSpec(maxWidth, View.MeasureSpec.AT_MOST);
34+
int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
3135

32-
view.measure(widthSpec, heightSpec);
33-
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
36+
view.measure(widthSpec, heightSpec);
37+
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
38+
prepared = true;
39+
}
3440
}
3541

36-
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end,
37-
float x, int top, int y, int bottom, @NonNull Paint paint) {
42+
@Override
43+
public void draw(@NonNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start,
44+
@IntRange(from = 0) int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
3845
prepView();
3946

4047
canvas.save();
41-
//Centering the token looks like a better strategy that aligning the bottom
42-
int padding = (bottom - top - view.getBottom()) / 2;
43-
canvas.translate(x, bottom - view.getBottom() - padding);
48+
canvas.translate(x, top);
4449
view.draw(canvas);
4550
canvas.restore();
4651
}
4752

48-
public int getSize(@NonNull Paint paint, CharSequence charSequence, int i, int i2, Paint.FontMetricsInt fm) {
53+
@Override
54+
public int getSize(@NonNull Paint paint, CharSequence charSequence, @IntRange(from = 0) int start,
55+
@IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fontMetricsInt) {
4956
prepView();
5057

51-
if (fm != null) {
58+
if (fontMetricsInt != null) {
5259
//We need to make sure the layout allots enough space for the view
5360
int height = view.getMeasuredHeight();
54-
int need = height - (fm.descent - fm.ascent);
55-
if (need > 0) {
56-
int ascent = need / 2;
57-
//This makes sure the text drawing area will be tall enough for the view
58-
fm.descent += need - ascent;
59-
fm.ascent -= ascent;
60-
fm.bottom += need - ascent;
61-
fm.top -= need / 2;
61+
62+
int adjustedBaseline = view.getBaseline();
63+
//-1 means the view doesn't support baseline alignment, so align bottom to font baseline
64+
if (adjustedBaseline == -1) {
65+
adjustedBaseline = height;
6266
}
67+
fontMetricsInt.ascent = fontMetricsInt.top = -adjustedBaseline;
68+
fontMetricsInt.descent = fontMetricsInt.bottom = height - adjustedBaseline;
6369
}
6470

6571
return view.getRight();

0 commit comments

Comments
 (0)