/* * 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.Canvas; import android.graphics.Paint; import android.graphics.Typeface; import android.support.annotation.ColorInt; import android.support.v4.view.GravityCompat; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import io.plaidapp.R; import io.plaidapp.util.FontUtil; /** * A view for displaying text that is will be overlapped by a Floating Action Button (FAB). * This view will indent itself at the given overlap point (as specified by * {@link #setFabOverlapGravity(int)}) to flow around it. * <p/> * Not actually a TextView but conforms to many of it's idioms. */ public class FabOverlapTextView extends View { private static final int DEFAULT_TEXT_SIZE_SP = 14; private int fabOverlapHeight; private int fabOverlapWidth; private int fabGravity; private int lineHeightHint; private int topPaddingHint; private StaticLayout layout; private CharSequence text; private TextPaint paint; private int fabId; private View fabView; public FabOverlapTextView(Context context) { super(context); init(context, null, 0, 0); } public FabOverlapTextView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0, 0); } public FabOverlapTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr, 0); } public FabOverlapTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs, defStyleAttr, defStyleRes); } private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { // Attribute initialization. paint = new TextPaint(Paint.ANTI_ALIAS_FLAG); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FabOverlapTextView); fabId = a.getResourceId(R.styleable.FabOverlapTextView_fabId, 0); setFabOverlapGravity(a.getInt(R.styleable.FabOverlapTextView_fabGravity, Gravity.BOTTOM | Gravity.RIGHT)); setFabOverlapHeight(a.getDimensionPixelSize(R.styleable .FabOverlapTextView_fabOverlayHeight, 0)); setFabOverlapWidth(a.getDimensionPixelSize(R.styleable .FabOverlapTextView_fabOverlayWidth, 0)); // TODO handle TextAppearance /*if (a.hasValue(R.styleable.FabOverlapTextView_android_textAppearance)) { final int textAppearanceId = a.getResourceId(R.styleable .FabOverlapTextView_android_textAppearance, android.R.style.TextAppearance); setTextAppearance(textAppearanceId); }*/ if (a.hasValue(R.styleable.FabOverlapTextView_font)) { setFont(a.getString(R.styleable.FabOverlapTextView_font)); } if (a.hasValue(R.styleable.FabOverlapTextView_android_textColor)) { setTextColor(a.getColor(R.styleable.FabOverlapTextView_android_textColor, 0)); } float defaultTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, DEFAULT_TEXT_SIZE_SP, getResources().getDisplayMetrics()); setTextSize(a.getDimensionPixelSize(R.styleable.FabOverlapTextView_android_textSize, (int) defaultTextSize)); lineHeightHint = a.getDimensionPixelSize(R.styleable.FabOverlapTextView_lineHeightHint, 0); topPaddingHint = a.getDimensionPixelSize(R.styleable.FabOverlapTextView_topPaddingHint, 0); a.recycle(); } public void setFabOverlapGravity(int fabGravity) { // we only really support [top|bottom][left|right|start|end] // TODO validate input this.fabGravity = GravityCompat.getAbsoluteGravity(fabGravity, getLayoutDirection()); } public void setFabOverlapHeight(int fabOverlapHeight) { this.fabOverlapHeight = fabOverlapHeight; } public void setFabOverlapWidth(int fabOverlapWidth) { this.fabOverlapWidth = fabOverlapWidth; } public void setText(CharSequence text) { this.text = text; layout = null; recompute(getWidth()); requestLayout(); } public void setTextSize(int textSize) { paint.setTextSize(textSize); } public void setTextColor(@ColorInt int color) { paint.setColor(color); } public void setTypeface(Typeface typeface) { paint.setTypeface(typeface); } public void setFont(String font) { setTypeface(FontUtil.get(getContext(), font)); } public void setLetterSpacing(float letterSpacing) { paint.setLetterSpacing(letterSpacing); } public void setFontFeatureSettings(String fontFeatureSettings) { paint.setFontFeatureSettings(fontFeatureSettings); } // @Override // protected void onAttachedToWindow() { // super.onAttachedToWindow(); // if (fabView == null) { // fabView = getRootView().findViewById(fabId); // } // } // // @Override // protected void onDetachedFromWindow() { // fabView = null; // super.onDetachedFromWindow(); // } private void recompute(int width) { if (text != null) { // work out the top padding and line height to align text to a 4dp grid float fourDip = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics()); // Ensure that the first line's baselines sits on 4dp grid by setting the top padding Paint.FontMetricsInt fm = paint.getFontMetricsInt(); int gridAlignedTopPadding = (int) (fourDip * (float) Math.ceil((topPaddingHint + Math.abs(fm.ascent)) / fourDip) - Math.ceil(Math.abs(fm.ascent))); setPadding(getPaddingLeft(), gridAlignedTopPadding, getPaddingRight(), getPaddingBottom()); // Ensures line height is a multiple of 4dp int fontHeight = Math.abs(fm.ascent - fm.descent) + fm.leading; int baselineAlignedLineHeight = (int) (fourDip * (float) Math.ceil(lineHeightHint / fourDip)); // before we can workout indents we need to know how many lines of text there are; so we // need to create a temporary layout :( layout = StaticLayout.Builder.obtain(text, 0, text.length(), paint, width) .setLineSpacing(baselineAlignedLineHeight - fontHeight, 1f) .build(); int preIndentedLineCount = layout.getLineCount(); /*int[] fabLocation = new int[2]; int[] ourLocation = new int[2]; fabView.getLocationOnScreen(fabLocation); getLocationOnScreen(ourLocation); ViewGroup.MarginLayoutParams fabLp = (ViewGroup.MarginLayoutParams) fabView .getLayoutParams(); int fabLeft = fabLocation[0] - fabLp.getMarginStart(); int fabRight = fabLocation[0] + fabView.getWidth() + fabLp.getMarginEnd(); int fabWidth = fabRight - fabLeft; int distanceFromLeft = fabLeft - ourLocation[0]; int distanceFromRight = ourLocation[0] + getWidth() - fabRight; boolean leftAlignedFab = distanceFromLeft < distanceFromRight; int[] leftIndents = new int[preIndentedLineCount]; int[] rightIndents = new int[preIndentedLineCount]; Rect fabRect = new Rect(fabLeft, fabLocation[1] - fabLp.topMargin, fabRight, fabLocation[1] + fabView.getHeight() + fabLp.bottomMargin); Rect lineRect = new Rect(ourLocation[0], ourLocation[1], ourLocation[0] + getWidth(), ourLocation[1] + baselineAlignedLineHeight); for (int line = 0; line < preIndentedLineCount; line++) { if (lineRect.intersect(fabRect)) { leftIndents[line] = leftAlignedFab ? fabWidth : 0; rightIndents[line] = leftAlignedFab ? 0 : fabWidth; } else { leftIndents[line] = 0; rightIndents[line] = 0; } lineRect.offset(0, baselineAlignedLineHeight); }*/ // now we can calculate the indents required for the given fab gravity boolean gravityTop = (fabGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.TOP; boolean gravityLeft = (fabGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT; // we want to iterate forward/backward over the lines depending on whether the fab // overlap vertical gravity is top/bottom int currentLine = gravityTop ? 0 : preIndentedLineCount - 1; int remainingHeightOverlap = fabOverlapHeight - (gravityTop ? getPaddingTop() : getPaddingBottom()); int[] leftIndents = new int[preIndentedLineCount]; int[] rightIndents = new int[preIndentedLineCount]; do { if (remainingHeightOverlap > 0) { // still have overlap height to consume, set the appropriate indent leftIndents[currentLine] = gravityLeft ? fabOverlapWidth : 0; rightIndents[currentLine] = gravityLeft ? 0 : fabOverlapWidth; remainingHeightOverlap -= baselineAlignedLineHeight; } else { // have consumed the overlap height: no indent leftIndents[currentLine] = 0; rightIndents[currentLine] = 0; } if (gravityTop) { // iterate forward over the lines currentLine++; } else { // iterate backward over the lines currentLine--; } } while (gravityTop ? currentLine < preIndentedLineCount : currentLine >= 0); // now that we know the indents, create the actual layout layout = StaticLayout.Builder.obtain(text, 0, text.length(), paint, width) .setLineSpacing(baselineAlignedLineHeight - fontHeight, 1f) .setIndents(leftIndents, rightIndents) .build(); if (layout.getLineCount() > preIndentedLineCount) { // adding indents has flown text onto a new line // TODO: ? } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) { throw new IllegalArgumentException("FabOverlapTextView requires a constrained width"); } int layoutWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); if (layout == null || layoutWidth != layout.getWidth()) { recompute(layoutWidth); } setMeasuredDimension(getPaddingLeft() + layout.getWidth() + getPaddingRight(), getPaddingTop() + layout.getHeight() + getPaddingBottom()); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (layout != null) { canvas.translate(getPaddingLeft(), getPaddingTop()); layout.draw(canvas); } } }