Skip to content

Instantly share code, notes, and snippets.

@romannurik
Last active December 17, 2021 03:15
Show Gist options
  • Save romannurik/6541192 to your computer and use it in GitHub Desktop.
Save romannurik/6541192 to your computer and use it in GitHub Desktop.
Demonstrates how to identify and avoid line-length issues with TextView. The measure, or characters per line, of a block of text plays a key role in how comfortable it is to read (sometimes referred to as readability). A widely accepted optimal range for a text block's measure is between 45 and 75 characters. This code demonstrates two phases of…
/*
* Copyright 2013 Google Inc.
*
* 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
*
* http://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.
*/
package com.example.android.charsperline;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.TextView;
public class CharsPerLineActivity extends Activity {
private static final String TAG = "CharsPerLineActivity";
private TextView mStatusView;
private View mContainerView;
private TextView mTextView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mStatusView = (TextView) findViewById(R.id.status);
mContainerView = findViewById(R.id.container);
mTextView = (TextView) findViewById(R.id.text);
mTextView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Log.d(TAG, "Parent: " + mContainerView.getWidth()
+ ", Child: " + mTextView.getWidth());
int maxCharsPerLine = CharsPerLineUtil.getMaxCharsPerLine(mTextView);
boolean badCpl = maxCharsPerLine < CharsPerLineUtil.RECOMMENDED_MIN_CPL
|| maxCharsPerLine > CharsPerLineUtil.RECOMMENDED_MAX_CPL;
mStatusView.setTextColor(badCpl ? 0xffff4444 : 0x88ffffff);
mStatusView.setText("Maximum measure: " + maxCharsPerLine + " CPL");
}
});
}
}
/*
* Copyright 2013 Google Inc.
*
* 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
*
* http://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.
*/
package com.example.android.charsperline;
import android.text.Layout;
import android.util.Log;
import android.widget.TextView;
/**
* Helper utilities useful during debugging that compute the maximum number of characters per line
* in a {@link TextView} or {@link Layout}.
*/
public class CharsPerLineUtil {
private static final String TAG = "CharsPerLineUtil";
public static final int RECOMMENDED_MIN_CPL = 45;
public static final int RECOMMENDED_MAX_CPL = 75;
/**
* Compute the maximum number of characters per line in the given {@link TextView}.
*/
public static int getMaxCharsPerLine(TextView textView) {
return getMaxCharsPerLine(textView.getLayout());
}
/**
* Compute the maximum number of characters per line in the given {@link Layout}.
*/
public static int getMaxCharsPerLine(Layout layout) {
int maxChars = 0;
int maxIndex = -1;
for (int i = layout.getLineCount() - 1; i >= 0; i--) {
int chars = layout.getLineEnd(i) - layout.getLineStart(i);
if (chars > maxChars) {
maxChars = chars;
maxIndex = i;
}
}
if (BuildConfig.DEBUG && maxIndex >= 0) {
CharSequence line = layout.getText().subSequence(
layout.getLineStart(maxIndex),
layout.getLineEnd(maxIndex));
Log.d(TAG, "Max line: '" + line + "' (length=" + line.length() + ")");
}
return maxChars;
}
}
/*
* Copyright 2013 Google Inc.
*
* 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
*
* http://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.
*/
package com.example.android.charsperline;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
/**
* A simple {@link LinearLayout} subclass, useful only in the vertical orientation, that allows each
* child to define a maximum width using a {@link LayoutParams#maxWidth layout_maxWidth} attribute
* (only useful if the child's {@link ViewGroup.LayoutParams#width layout_width} is {@link
* ViewGroup.LayoutParams#MATCH_PARENT}).
*/
public class MaxWidthLinearLayout extends LinearLayout {
public MaxWidthLinearLayout(Context context) {
super(context);
}
public MaxWidthLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MaxWidthLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Temporarily assign a new {@link ViewGroup.LayoutParams#width} to the maximum width if the
* child would normally exceed the maximum width.
*/
private void assignTemporaryChildWidthDuringMeasure(View child, int parentWidthMeasureSpec) {
LayoutParams lp = (LayoutParams) child.getLayoutParams();
assert lp != null;
int availableWidth = MeasureSpec.getSize(parentWidthMeasureSpec);
if (lp.width == LayoutParams.MATCH_PARENT && availableWidth > lp.maxWidth) {
lp.oldLayoutWidth = LayoutParams.MATCH_PARENT;
lp.width = Math.min(lp.maxWidth, availableWidth);
}
}
/**
* Revert any changes caused by {@link #assignTemporaryChildWidthDuringMeasure(android.view.View,
* int)}.
*/
private void revertChildWidthDuringMeasure(View child) {
LayoutParams lp = (LayoutParams) child.getLayoutParams();
assert lp != null;
if (lp.oldLayoutWidth != Integer.MIN_VALUE) {
lp.width = lp.oldLayoutWidth;
}
}
@Override
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
assignTemporaryChildWidthDuringMeasure(child, parentWidthMeasureSpec);
super.measureChild(child, parentWidthMeasureSpec, parentHeightMeasureSpec);
revertChildWidthDuringMeasure(child);
}
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec,
int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
assignTemporaryChildWidthDuringMeasure(child, parentWidthMeasureSpec);
super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
parentHeightMeasureSpec, heightUsed);
revertChildWidthDuringMeasure(child);
}
@Override
protected MaxWidthLinearLayout.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
public MaxWidthLinearLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MaxWidthLinearLayout.LayoutParams(getContext(), attrs);
}
@Override
protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new MaxWidthLinearLayout.LayoutParams(lp);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
return lp instanceof MaxWidthLinearLayout.LayoutParams;
}
/**
* Provides additional layout params (specifically {@link #maxWidth layout_maxWidth}) for
* children of {@link MaxWidthLinearLayout}.
*/
public static class LayoutParams extends LinearLayout.LayoutParams {
private int maxWidth;
private int oldLayoutWidth = Integer.MIN_VALUE; // used to store old lp.width during measure
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.MaxWidthLinearLayout_Layout);
assert a != null;
maxWidth = a.getLayoutDimension(
R.styleable.MaxWidthLinearLayout_Layout_layout_maxWidth, Integer.MAX_VALUE);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(int width, int height, int gravity) {
super(width, height, gravity);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
}
}
<!--
Copyright 2013 Google Inc.
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
http://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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.android.charsperline.MaxWidthLinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:paddingBottom="48dp"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/lorem_ipsum"
android:textSize="18sp"
android:lineSpacingMultiplier="1.1"
android:fontFamily="sans-serif-light"
app:layout_maxWidth="400sp" />
</com.example.android.charsperline.MaxWidthLinearLayout>
</ScrollView>
<TextView android:id="@+id/status"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_gravity="bottom"
android:gravity="center"
android:background="#d000"
android:textSize="16sp"
android:fontFamily="sans-serif-condensed"
android:textStyle="bold|italic" />
</FrameLayout>
<!--
Copyright 2013 Google Inc.
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
http://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>
<declare-styleable name="MaxWidthLinearLayout_Layout">
<attr name="layout_maxWidth" format="dimension" />
</declare-styleable>
</resources>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment