/* * Copyright 2015 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 io.plaidapp.ui.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Typeface; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.util.AttributeSet; import android.util.TypedValue; import android.widget.TextView; import io.plaidapp.R; /** * TODO: document your custom view class. */ public class DynamicTextView extends TextView { private static MaterialTypeStyle[] mStyles = { new MaterialTypeStyle(112, "sans-serif-light", 0x8a), /* Display 4 */ new MaterialTypeStyle(56, "sans-serif", 0x8a), /* Display 3 */ new MaterialTypeStyle(45, "sans-serif", 0x8a), /* Display 2 */ new MaterialTypeStyle(34, "sans-serif", 0x8a), /* Display 1 */ new MaterialTypeStyle(24, "sans-serif", 0xde), /* Headline */ new MaterialTypeStyle(20, "sans-serif-medium", 0xde) /* Title */ }; private boolean mSnapToMaterialScale; private int mMinTextSize; private int mMaxTextSize; private float scaledDensity; private boolean mCalculated = false; public DynamicTextView(Context context) { super(context); init(null, 0); } public DynamicTextView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs, 0); } public DynamicTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(attrs, defStyle); } private void init(AttributeSet attrs, int defStyle) { scaledDensity = getContext().getResources().getDisplayMetrics().scaledDensity; // Load attributes final TypedArray a = getContext().obtainStyledAttributes( attrs, R.styleable.DynamicTextView, defStyle, 0); mSnapToMaterialScale = a.getBoolean(R.styleable.DynamicTextView_snapToMaterialScale, true); mMinTextSize = a.getDimensionPixelSize( R.styleable.DynamicTextView_minTextSize, (int) (20 * scaledDensity)); mMaxTextSize = a.getDimensionPixelSize( R.styleable.DynamicTextView_maxTextSize, (int) (112 * scaledDensity)); a.recycle(); } private void fitText() { // different methods for achieving this depending on whether we are snapping to the material // scale, and if multiple lines are allowed. 4 method for the permutations of this. if (mSnapToMaterialScale && getMaxLines() == 1) { // technically we could use the multi line algorithm here but this is more efficient fitSnappedSingleLine(); } else if (mSnapToMaterialScale) { fitSnappedMultiLine(); } else if (!mSnapToMaterialScale && getMaxLines() == 1) { fitSingleLine(); } else if (!mSnapToMaterialScale) { fitMultiline(); } } private void fitSnappedMultiLine() { int targetWidth = getWidth() - getPaddingLeft() - getPaddingRight(); int targetHeight = getHeight() - getPaddingTop() - getPaddingBottom(); if (targetWidth > 0 && targetHeight > 0) { int style = 0; MaterialTypeStyle currentStyle = mStyles[style]; TextPaint paint = getPaint(); StaticLayout staticLayout = null; int currentHeight = Integer.MAX_VALUE; int lines = 0; boolean maxLinesSet = getMaxLines() != Integer.MAX_VALUE; while ((currentHeight > targetHeight || (maxLinesSet && lines > getMaxLines())) && style <= mStyles.length - 1 && currentStyle.size * scaledDensity >= mMinTextSize && currentStyle.size * scaledDensity <= mMaxTextSize) { currentStyle = mStyles[style]; paint.setTextSize(currentStyle.size * scaledDensity); paint.setTypeface(Typeface.create(currentStyle.fontFamily, Typeface.NORMAL)); staticLayout = new StaticLayout(getText(), paint, targetWidth, Layout.Alignment .ALIGN_NORMAL, 1.0f, 0.0f, true); currentHeight = staticLayout.getHeight(); lines = staticLayout.getLineCount(); style++; } super.setTextSize(TypedValue.COMPLEX_UNIT_SP, currentStyle.size); setTypeface(Typeface.create(currentStyle.fontFamily, Typeface.NORMAL)); int currentColour = getCurrentTextColor(); setTextColor(Color.argb(currentStyle.opacity, Color.red(currentColour), Color.green(currentColour), Color.blue(currentColour))); if (style == mStyles.length) { setEllipsize(TextUtils.TruncateAt.END); } if (currentStyle.size * scaledDensity < mMinTextSize) { // wanted to make text smaller but hit min text size. Need to set max lines. setMaxLines((int) Math.floor((((float) targetHeight / (float) currentHeight) * lines))); setEllipsize(TextUtils.TruncateAt.END); } setTextAlignment(TEXT_ALIGNMENT_TEXT_START); mCalculated = true; } } private void fitSnappedSingleLine() { int targetWidth = getWidth() - getPaddingLeft() - getPaddingRight(); if (targetWidth > 0) { int style = 0; TextPaint paint = getPaint(); final String text = getText().toString(); MaterialTypeStyle currentStyle = null; float currentWidth = Float.MAX_VALUE; while (currentWidth > targetWidth && style < mStyles.length) { currentStyle = mStyles[style]; paint.setTextSize(currentStyle.size * scaledDensity); paint.setTypeface(Typeface.create(currentStyle.fontFamily, Typeface.NORMAL)); currentWidth = paint.measureText(text); style++; } setTextSize(TypedValue.COMPLEX_UNIT_SP, currentStyle.size); setTypeface(Typeface.create(currentStyle.fontFamily, Typeface.NORMAL)); int currentColour = getCurrentTextColor(); setTextColor(Color.argb(currentStyle.opacity, Color.red(currentColour), Color.green(currentColour), Color.blue(currentColour))); if (style == mStyles.length) { setEllipsize(TextUtils.TruncateAt.END); } mCalculated = true; } } private void fitMultiline() { int targetWidth = getWidth() - getPaddingLeft() - getPaddingRight(); int targetHeight = getHeight() - getPaddingTop() - getPaddingBottom(); if (targetWidth > 0 && targetHeight > 0) { int textSize = mMaxTextSize; TextPaint paint = getPaint(); paint.setTextSize(textSize); StaticLayout staticLayout = new StaticLayout(getText(), paint, targetWidth, Layout .Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true); int currentHeight = staticLayout.getHeight(); while (currentHeight > targetHeight && textSize > mMinTextSize) { textSize--; paint.setTextSize(textSize); staticLayout = new StaticLayout(getText(), paint, targetWidth, Layout.Alignment .ALIGN_NORMAL, 1.0f, 0.0f, true); currentHeight = staticLayout.getHeight(); } setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); setTextAlignment(TEXT_ALIGNMENT_TEXT_START); mCalculated = true; } } private void fitSingleLine() { int targetWidth = getWidth() - getPaddingLeft() - getPaddingRight(); if (targetWidth > 0) { int textSize = mMaxTextSize; TextPaint paint = getPaint(); paint.setTextSize(textSize); final String text = getText().toString(); float currentWidth = paint.measureText(text); while (currentWidth > targetWidth && textSize > mMinTextSize) { textSize--; paint.setTextSize(textSize); currentWidth = paint.measureText(text); } setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); mCalculated = true; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (!mCalculated) { fitText(); } } public boolean isSnapToMaterialScale() { return mSnapToMaterialScale; } public void setSnapToMaterialScale(boolean snapToMaterialScale) { this.mSnapToMaterialScale = snapToMaterialScale; } public int getMinTextSize() { return mMinTextSize; } public void setMinTextSize(int minTextSize) { this.mMinTextSize = minTextSize; } public int getMaxTextSize() { return mMaxTextSize; } public void setMaxTextSize(int maxTextSize) { this.mMaxTextSize = maxTextSize; } private static class MaterialTypeStyle { int size; String fontFamily; int opacity; MaterialTypeStyle(int size, String fontFamily, int opacity) { this.size = size; this.fontFamily = fontFamily; this.opacity = opacity; } } }