/*
* Copyright (C) 2014 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
*
* 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 android.support.v17.leanback.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.text.Layout;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;
import android.support.v17.leanback.R;
/**
* <p>A {@link android.widget.TextView} that adjusts text size automatically in response
* to certain trigger conditions, such as text that wraps over multiple lines.</p>
* @hide
*/
class ResizingTextView extends TextView {
/**
* Trigger text resize when text flows into the last line of a multi-line text view.
*/
public static final int TRIGGER_MAX_LINES = 0x01;
private int mTriggerConditions; // Union of trigger conditions
private int mResizedTextSize;
// Note: Maintaining line spacing turned out not to be useful, and will be removed in
// the next round of design for this class (b/18736630). For now it simply defaults to false.
private boolean mMaintainLineSpacing;
private int mResizedPaddingAdjustmentTop;
private int mResizedPaddingAdjustmentBottom;
private boolean mIsResized = false;
// Remember default properties in case we need to restore them
private boolean mDefaultsInitialized = false;
private int mDefaultTextSize;
private float mDefaultLineSpacingExtra;
private int mDefaultPaddingTop;
private int mDefaultPaddingBottom;
public ResizingTextView(Context ctx, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(ctx, attrs, defStyleAttr);
TypedArray a = ctx.obtainStyledAttributes(attrs, R.styleable.lbResizingTextView,
defStyleAttr, defStyleRes);
try {
mTriggerConditions = a.getInt(
R.styleable.lbResizingTextView_resizeTrigger, TRIGGER_MAX_LINES);
mResizedTextSize = a.getDimensionPixelSize(
R.styleable.lbResizingTextView_resizedTextSize, -1);
mMaintainLineSpacing = a.getBoolean(
R.styleable.lbResizingTextView_maintainLineSpacing, false);
mResizedPaddingAdjustmentTop = a.getDimensionPixelOffset(
R.styleable.lbResizingTextView_resizedPaddingAdjustmentTop, 0);
mResizedPaddingAdjustmentBottom = a.getDimensionPixelOffset(
R.styleable.lbResizingTextView_resizedPaddingAdjustmentBottom, 0);
} finally {
a.recycle();
}
}
public ResizingTextView(Context ctx, AttributeSet attrs, int defStyleAttr) {
this(ctx, attrs, defStyleAttr, 0);
}
public ResizingTextView(Context ctx, AttributeSet attrs) {
// TODO We should define our own style that inherits from TextViewStyle, to set defaults
// for new styleables, We then pass the appropriate R.attr up the constructor chain here.
this(ctx, attrs, android.R.attr.textViewStyle);
}
public ResizingTextView(Context ctx) {
this(ctx, null);
}
/**
* @return the trigger conditions used to determine whether resize occurs
*/
public int getTriggerConditions() {
return mTriggerConditions;
}
/**
* Set the trigger conditions used to determine whether resize occurs. Pass
* a union of trigger condition constants, such as {@link ResizingTextView#TRIGGER_MAX_LINES}.
*
* @param conditions A union of trigger condition constants
*/
public void setTriggerConditions(int conditions) {
if (mTriggerConditions != conditions) {
mTriggerConditions = conditions;
// Always request a layout when trigger conditions change
requestLayout();
}
}
/**
* @return the resized text size
*/
public int getResizedTextSize() {
return mResizedTextSize;
}
/**
* Set the text size for resized text.
*
* @param size The text size for resized text
*/
public void setResizedTextSize(int size) {
if (mResizedTextSize != size) {
mResizedTextSize = size;
resizeParamsChanged();
}
}
/**
* @return whether or not to maintain line spacing when resizing text.
* The default is true.
*/
public boolean getMaintainLineSpacing() {
return mMaintainLineSpacing;
}
/**
* Set whether or not to maintain line spacing when resizing text.
* The default is true.
*
* @param maintain Whether or not to maintain line spacing
*/
public void setMaintainLineSpacing(boolean maintain) {
if (mMaintainLineSpacing != maintain) {
mMaintainLineSpacing = maintain;
resizeParamsChanged();
}
}
/**
* @return desired adjustment to top padding for resized text
*/
public int getResizedPaddingAdjustmentTop() {
return mResizedPaddingAdjustmentTop;
}
/**
* Set the desired adjustment to top padding for resized text.
*
* @param adjustment The adjustment to top padding, in pixels
*/
public void setResizedPaddingAdjustmentTop(int adjustment) {
if (mResizedPaddingAdjustmentTop != adjustment) {
mResizedPaddingAdjustmentTop = adjustment;
resizeParamsChanged();
}
}
/**
* @return desired adjustment to bottom padding for resized text
*/
public int getResizedPaddingAdjustmentBottom() {
return mResizedPaddingAdjustmentBottom;
}
/**
* Set the desired adjustment to bottom padding for resized text.
*
* @param adjustment The adjustment to bottom padding, in pixels
*/
public void setResizedPaddingAdjustmentBottom(int adjustment) {
if (mResizedPaddingAdjustmentBottom != adjustment) {
mResizedPaddingAdjustmentBottom = adjustment;
resizeParamsChanged();
}
}
private void resizeParamsChanged() {
// If we're not resized, then changing resize parameters doesn't
// affect layout, so don't bother requesting.
if (mIsResized) {
requestLayout();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!mDefaultsInitialized) {
mDefaultTextSize = (int) getTextSize();
mDefaultLineSpacingExtra = getLineSpacingExtra();
mDefaultPaddingTop = getPaddingTop();
mDefaultPaddingBottom = getPaddingBottom();
mDefaultsInitialized = true;
}
// Always try first to measure with defaults. Otherwise, we may think we can get away
// with larger text sizes later when we actually can't.
setTextSize(TypedValue.COMPLEX_UNIT_PX, mDefaultTextSize);
setLineSpacing(mDefaultLineSpacingExtra, getLineSpacingMultiplier());
setPaddingTopAndBottom(mDefaultPaddingTop, mDefaultPaddingBottom);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
boolean resizeText = false;
final Layout layout = getLayout();
if (layout != null) {
if ((mTriggerConditions & TRIGGER_MAX_LINES) > 0) {
final int lineCount = layout.getLineCount();
final int maxLines = getMaxLines();
if (maxLines > 1) {
resizeText = lineCount == maxLines;
}
}
}
final int currentSizePx = (int) getTextSize();
boolean remeasure = false;
if (resizeText) {
if (mResizedTextSize != -1 && currentSizePx != mResizedTextSize) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, mResizedTextSize);
remeasure = true;
}
// Check for other desired adjustments in addition to the text size
final float targetLineSpacingExtra = mDefaultLineSpacingExtra +
mDefaultTextSize - mResizedTextSize;
if (mMaintainLineSpacing && getLineSpacingExtra() != targetLineSpacingExtra) {
setLineSpacing(targetLineSpacingExtra, getLineSpacingMultiplier());
remeasure = true;
}
final int paddingTop = mDefaultPaddingTop + mResizedPaddingAdjustmentTop;
final int paddingBottom = mDefaultPaddingBottom + mResizedPaddingAdjustmentBottom;
if (getPaddingTop() != paddingTop || getPaddingBottom() != paddingBottom) {
setPaddingTopAndBottom(paddingTop, paddingBottom);
remeasure = true;
}
} else {
// Use default size, line spacing, and padding
if (mResizedTextSize != -1 && currentSizePx != mDefaultTextSize) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, mDefaultTextSize);
remeasure = true;
}
if (mMaintainLineSpacing && getLineSpacingExtra() != mDefaultLineSpacingExtra) {
setLineSpacing(mDefaultLineSpacingExtra, getLineSpacingMultiplier());
remeasure = true;
}
if (getPaddingTop() != mDefaultPaddingTop ||
getPaddingBottom() != mDefaultPaddingBottom) {
setPaddingTopAndBottom(mDefaultPaddingTop, mDefaultPaddingBottom);
remeasure = true;
}
}
mIsResized = resizeText;
if (remeasure) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
private void setPaddingTopAndBottom(int paddingTop, int paddingBottom) {
if (isPaddingRelative()) {
setPaddingRelative(getPaddingStart(), paddingTop, getPaddingEnd(), paddingBottom);
} else {
setPadding(getPaddingLeft(), paddingTop, getPaddingRight(), paddingBottom);
}
}
}