Skip to content

Commit

Permalink
Added support to automatically inflate MaterialTextView component int…
Browse files Browse the repository at this point in the history
…o TextView.

Added support to prevent applying line heights from text appearance styles if the current theme sets the flag 'textAppearanceLineHeightEnabled' to false.

PiperOrigin-RevId: 256216184
  • Loading branch information
raajkumars committed Jul 3, 2019
1 parent 3753456 commit d7a9248
Show file tree
Hide file tree
Showing 14 changed files with 212 additions and 55 deletions.
1 change: 0 additions & 1 deletion build.gradle
Expand Up @@ -45,7 +45,6 @@ ext {
espressoVersion = '3.1.0'
mockitoCoreVersion = '2.25.0'
truthVersion = '0.45'
junitVersion = '1.1.1'

// Enforce the use of prebuilt dependencies in all sub-projects. This is
// required for the doclava dependency.
Expand Down
Expand Up @@ -29,6 +29,7 @@
import androidx.annotation.StyleableRes;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.TintTypedArray;
import android.util.TypedValue;

/**
* Utility methods to resolve resources for components.
Expand Down Expand Up @@ -136,6 +137,36 @@ public static TextAppearance getTextAppearance(
return null;
}

/**
* Retrieve a dimensional unit attribute at <var>index</var> for use as a size in raw pixels. A
* size conversion involves rounding the base value, and ensuring that a non-zero base value is at
* least one pixel in size.
*
* <p>This method will throw an exception if the attribute is defined but is not a dimension.
*
* @param context The Context the view is running in, through which the current theme, resources,
* etc can be accessed.
* @param attributes array of typed attributes from which the dimension unit must be read.
* @param index Index of attribute to retrieve.
* @param defaultValue Value to return if the attribute is not defined or not a resource.
* @return Attribute dimension value multiplied by the appropriate metric and truncated to integer
* pixels, or defaultValue if not defined.
* @throws UnsupportedOperationException if the attribute is defined but is not a dimension.
* @see TypedArray#getDimensionPixelSize(int, int)
*/
public static int getDimensionPixelSize(
Context context, TypedArray attributes, @StyleableRes int index, final int defaultValue) {
TypedValue value = new TypedValue();
if (!attributes.getValue(index, value) || value.type != TypedValue.TYPE_ATTRIBUTE) {
return attributes.getDimensionPixelSize(index, defaultValue);
}

TypedArray styledAttrs = context.getTheme().obtainStyledAttributes(new int[] {value.data});
int dimension = styledAttrs.getDimensionPixelSize(0, defaultValue);
styledAttrs.recycle();
return dimension;
}

/**
* Returns the @StyleableRes index that contains value in the attributes array. If both indices
* contain values, the first given index takes precedence and is returned.
Expand Down
80 changes: 42 additions & 38 deletions lib/java/com/google/android/material/textview/MaterialTextView.java
Expand Up @@ -22,14 +22,15 @@
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import androidx.annotation.StyleableRes;
import com.google.android.material.resources.MaterialAttributes;
import com.google.android.material.resources.MaterialResources;
import androidx.appcompat.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.widget.EditText;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* A MaterialTextView is a derivative of {@link AppCompatTextView} that displays text to the user.
* To provide user-editable text, see {@link EditText}.
* To provide user-editable text, see {@link android.widget.EditText}.
*
* <p>MaterialTextView supports the ability to read and apply {@code android:lineHeight} value from
* a {@code TextAppearance} style.
Expand Down Expand Up @@ -70,13 +71,6 @@
*/
public class MaterialTextView extends AppCompatTextView {

/**
* Flag that will be used to avoid applying line height more than once when {@link
* #setTextAppearance(int)} method is called, the base implementation of which may call the {@link
* #setTextAppearance(Context, int)} internally.
*/
private final AtomicBoolean skipApplyingLineHeight = new AtomicBoolean();

public MaterialTextView(Context context) {
this(context, null /* attrs */);
}
Expand All @@ -92,61 +86,71 @@ public MaterialTextView(Context context, AttributeSet attrs, int defStyleAttr) {
public MaterialTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr);

final Resources.Theme theme = context.getTheme();
if (canApplyTextAppearanceLineHeight(context)) {
final Resources.Theme theme = context.getTheme();

if (readLineHeightFromLayout(theme, attrs, defStyleAttr, defStyleRes) < 0) {
int resId = findViewAppearanceResourceId(theme, attrs, defStyleAttr, defStyleRes);
if (resId != -1) {
applyLineHeightFromViewAppearance(theme, resId);
if (!viewAttrsHasLineHeight(context, theme, attrs, defStyleAttr, defStyleRes)) {
int resId = findViewAppearanceResourceId(theme, attrs, defStyleAttr, defStyleRes);
if (resId != -1) {
applyLineHeightFromViewAppearance(theme, resId);
}
}
}
}

@Override
public void setTextAppearance(int resId) {
skipApplyingLineHeight.set(true);
super.setTextAppearance(resId);
skipApplyingLineHeight.set(false);
applyLineHeightFromViewAppearance(getContext().getTheme(), resId);
}

@Override
public void setTextAppearance(Context context, int resId) {
super.setTextAppearance(context, resId);
if (!skipApplyingLineHeight.get()) {

if (canApplyTextAppearanceLineHeight(context)) {
applyLineHeightFromViewAppearance(context.getTheme(), resId);
}
}

private void applyLineHeightFromViewAppearance(Theme theme, int resId) {
TypedArray attributes = theme.obtainStyledAttributes(resId, R.styleable.MaterialTextAppearance);
int lineHeight = readLineHeighAttribute(attributes);
int lineHeight =
readFirstAvailableDimension(
getContext(),
attributes,
R.styleable.MaterialTextAppearance_android_lineHeight,
R.styleable.MaterialTextAppearance_lineHeight);
attributes.recycle();

if (lineHeight >= 0) {
setLineHeight(lineHeight);
}
}

private static int readLineHeightFromLayout(
Theme theme, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray attributes =
theme.obtainStyledAttributes(
attrs, R.styleable.MaterialTextView, defStyleAttr, defStyleRes);
int lineHeight = readLineHeighAttribute(attributes);
attributes.recycle();
private static boolean canApplyTextAppearanceLineHeight(Context context) {
return MaterialAttributes.resolveBoolean(context, R.attr.textAppearanceLineHeightEnabled, true);
}

private static int readFirstAvailableDimension(
Context context, TypedArray attributes, @StyleableRes int... indices) {
int lineHeight = -1;

for (int index = 0; index < indices.length && lineHeight < 0; ++index) {
lineHeight = MaterialResources.getDimensionPixelSize(context, attributes, indices[index], -1);
}

return lineHeight;
}

private static int readLineHeighAttribute(TypedArray attributes) {
private static boolean viewAttrsHasLineHeight(
Context context, Theme theme, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray attributes =
theme.obtainStyledAttributes(
attrs, R.styleable.MaterialTextView, defStyleAttr, defStyleRes);
int lineHeight =
attributes.getDimensionPixelSize(R.styleable.MaterialTextView_android_lineHeight, -1);
if (lineHeight < 0) {
lineHeight = attributes.getDimensionPixelSize(R.styleable.MaterialTextView_lineHeight, -1);
}
readFirstAvailableDimension(
context,
attributes,
R.styleable.MaterialTextView_android_lineHeight,
R.styleable.MaterialTextView_lineHeight);
attributes.recycle();

return lineHeight;
return lineHeight != -1;
}

private static int findViewAppearanceResourceId(
Expand Down
3 changes: 0 additions & 3 deletions lib/java/com/google/android/material/textview/build.gradle
@@ -1,9 +1,6 @@
apply plugin: 'com.android.library'
apply plugin: 'com.github.dcendents.android-maven'

archivesBaseName = getArchivesBaseName(project.name)
version = rootProject.ext.mdcLibraryVersion

dependencies {
implementation compatibility("appcompat")

Expand Down
Expand Up @@ -16,6 +16,12 @@
-->
<resources>

<!--
Setting this attribute value to false in the theme, will cause the MaterialTextView to ignore
line height specified in all text appearance styles. The value is true by default.
-->
<attr name="textAppearanceLineHeightEnabled" format="boolean" />

<declare-styleable name="MaterialTextAppearance" parent="TextAppearance">
<!--
Specifies explicit line height for this TextView. This is equivalent to the vertical
Expand Down
Expand Up @@ -26,9 +26,11 @@
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.AppCompatCheckBox;
import androidx.appcompat.widget.AppCompatRadioButton;
import androidx.appcompat.widget.AppCompatTextView;
import android.util.AttributeSet;
import com.google.android.material.checkbox.MaterialCheckBox;
import com.google.android.material.radiobutton.MaterialRadioButton;
import com.google.android.material.textview.MaterialTextView;

/**
* An extension of {@link AppCompatViewInflater} that replaces some framework widgets with Material
Expand Down Expand Up @@ -94,4 +96,10 @@ protected AppCompatCheckBox createCheckBox(Context context, AttributeSet attrs)
protected AppCompatRadioButton createRadioButton(Context context, AttributeSet attrs) {
return new MaterialRadioButton(context, attrs);
}

@NonNull
@Override
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
return new MaterialTextView(context, attrs);
}
}
Expand Up @@ -48,38 +48,64 @@ public void readTestLineHeights() {
resources.getDimensionPixelSize(R.dimen.material_text_view_test_line_height_override);
}

@Test
public void ensureThatViewCanBeCreatedWithoutSettingLineHeightAttribute() {
inflater.inflate(R.layout.text_view_without_line_height, null, false);
}

@Test
public void ensureThatCreatedViewUsesLineHeightFromThemeAttribute() {
context.setTheme(R.style.TestThemeWithLineHeight);
MaterialTextView textView =
(MaterialTextView) inflater.inflate(R.layout.text_view_with_theme_line_height, null, false);
assertThat(textView.getLineHeight()).isEqualTo(testLineHeight);
}

@Test
public void ensureThatCreatedViewUsesLineHeightFromTextAppearance() {
MaterialTextView textView =
(MaterialTextView)
inflater.inflate(R.layout.text_view_with_line_height_from_appearance, null, false);
final int actualLineHeight = textView.getLineHeight();
assertThat(actualLineHeight).isEqualTo(testLineHeight);
assertThat(textView.getLineHeight()).isEqualTo(testLineHeight);
}

@Test
public void ensureThatCreatedViewUsesLineHeightFromStyleWithTextAppearance() {
MaterialTextView textView =
(MaterialTextView)
inflater.inflate(R.layout.text_view_with_line_height_from_style, null, false);
final int actualLineHeight = textView.getLineHeight();
assertThat(actualLineHeight).isEqualTo(testLineHeight);
assertThat(textView.getLineHeight()).isEqualTo(testLineHeight);
}

@Test
public void ensureThatLineHeightFromLayoutOverridesThatFromTextAppearance() {
MaterialTextView textView =
(MaterialTextView)
inflater.inflate(R.layout.text_view_with_line_height_from_layout, null, false);
final int actualLineHeight = textView.getLineHeight();
assertThat(actualLineHeight).isEqualTo(testLineHeightOverride);
assertThat(textView.getLineHeight()).isEqualTo(testLineHeightOverride);
}

@Test
public void ensureThatViewAppliesLineHeightWhenSettingTextAppearance() {
MaterialTextView textView = new MaterialTextView(context);
textView.setTextAppearance(context, R.style.TestStyleWithLineHeight);
final int actualLineHeight = textView.getLineHeight();
assertThat(actualLineHeight).isEqualTo(testLineHeight);
assertThat(textView.getLineHeight()).isEqualTo(testLineHeight);
}

@Test
public void ensureThatCreatedViewIgnoresLineHeightFromTextAppearanceIfLineHeightIsDisabled() {
context.setTheme(R.style.TestThemeWithLineHeightDisabled);
MaterialTextView textView =
(MaterialTextView)
inflater.inflate(R.layout.text_view_with_line_height_from_appearance, null, false);
assertThat(textView.getLineHeight()).isNotEqualTo(testLineHeight);
}

@Test
public void ensureThatViewIgnoreLineHeightWhenSettingTextAppearanceIfLineHeightIsDisabled() {
context.setTheme(R.style.TestThemeWithLineHeightDisabled);
MaterialTextView textView = new MaterialTextView(context);
textView.setTextAppearance(context, R.style.TestStyleWithLineHeight);
assertThat(textView.getLineHeight()).isNotEqualTo(testLineHeight);
}
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<com.google.android.material.textview.MaterialTextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TestStyleWithThemeLineHeightAttribute"/>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<com.google.android.material.textview.MaterialTextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Expand Up @@ -27,4 +27,9 @@
<style name="TestStyleWithLineHeightAppearance">
<item name="android:textAppearance">@style/TestStyleWithLineHeight</item>
</style>

<style name="TestStyleWithThemeLineHeightAttribute" parent="TestStyleWithoutLineHeight">
<item name="lineHeight">?attr/themeLineHeight</item>
</style>

</resources>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>

<attr name="themeLineHeight" format="dimension" />

<style name="TestThemeWithLineHeight" parent="Theme.AppCompat.Light">
<item name="themeLineHeight">@dimen/material_text_view_test_line_height</item>
</style>

<style name="TestThemeWithLineHeightDisabled" parent="TestThemeWithLineHeight">
<item name="textAppearanceLineHeightEnabled">false</item>
</style>
</resources>

0 comments on commit d7a9248

Please sign in to comment.