/*
* 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.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.v4.view.GravityCompat;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import in.uncod.android.bypass.style.TouchableUrlSpan;
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.
*
* Not actually a TextView but conforms to many of it's idioms.
*/
@TargetApi(Build.VERSION_CODES.M)
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 unalignedTopPadding;
private int unalignedBottomPadding;
private int breakStrategy;
private StaticLayout layout;
private CharSequence text;
private TextPaint paint;
private TouchableUrlSpan pressedSpan;
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) {
paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FabOverlapTextView);
float defaultTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
DEFAULT_TEXT_SIZE_SP, getResources().getDisplayMetrics());
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));
if (a.hasValue(R.styleable.FabOverlapTextView_android_textAppearance)) {
final int textAppearance = a.getResourceId(
R.styleable.FabOverlapTextView_android_textAppearance,
android.R.style.TextAppearance);
TypedArray atp = getContext().obtainStyledAttributes(textAppearance,
R.styleable.FontTextAppearance);
paint.setColor(atp.getColor(R.styleable.FontTextAppearance_android_textColor,
Color.BLACK));
paint.setTextSize(atp.getDimensionPixelSize(
R.styleable.FontTextAppearance_android_textSize, (int) defaultTextSize));
if (atp.hasValue(R.styleable.FontTextAppearance_font)) {
paint.setTypeface(FontUtil.get(getContext(),
atp.getString(R.styleable.FontTextAppearance_font)));
}
atp.recycle();
}
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));
}
if (a.hasValue(R.styleable.FabOverlapTextView_android_textSize)) {
setTextSize(a.getDimensionPixelSize(R.styleable.FabOverlapTextView_android_textSize,
(int) defaultTextSize));
}
lineHeightHint = a.getDimensionPixelSize(R.styleable.FabOverlapTextView_lineHeightHint, 0);
unalignedTopPadding = getPaddingTop();
unalignedBottomPadding = getPaddingBottom();
breakStrategy = a.getInt(R.styleable.FabOverlapTextView_android_breakStrategy,
Layout.BREAK_STRATEGY_BALANCED);
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);
}
private void recompute(int width) {
if (text != null) {
// work out the top padding and line height to align text to a 4dp grid
final 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
final Paint.FontMetricsInt fm = paint.getFontMetricsInt();
final int gridAlignedTopPadding = (int) (fourDip * (float)
Math.ceil((unalignedTopPadding + Math.abs(fm.ascent)) / fourDip)
- Math.ceil(Math.abs(fm.ascent)));
super.setPadding(
getPaddingLeft(), gridAlignedTopPadding, getPaddingTop(), getPaddingBottom());
// ensures line height is a multiple of 4dp
final int fontHeight = Math.abs(fm.ascent - fm.descent) + fm.leading;
final 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)
.setBreakStrategy(breakStrategy)
.build();
final int preIndentedLineCount = layout.getLineCount();
// now we can calculate the indents required for the given fab gravity
final boolean gravityTop = (fabGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.TOP;
final 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());
final int[] leftIndents = new int[preIndentedLineCount];
final 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)
.setBreakStrategy(breakStrategy)
.build();
// ensure that the view's height sits on the grid (as we've changed padding etc).
final int height = getPaddingTop() + layout.getHeight() + getPaddingBottom();
final float overhang = height % fourDip;
if (overhang != 0) {
super.setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
unalignedBottomPadding + (int) (fourDip - overhang));
}
}
}
@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 != null ? layout.getWidth() : 0) + getPaddingRight(),
getPaddingTop() + (layout != null ? layout.getHeight() : 0) + getPaddingBottom());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (layout != null) {
canvas.translate(getPaddingLeft(), getPaddingTop());
layout.draw(canvas);
}
}
@Override
public void setPadding(int left, int top, int right, int bottom) {
super.setPadding(left, top, right, bottom);
unalignedTopPadding = top;
unalignedBottomPadding = bottom;
if (layout != null) recompute(layout.getWidth());
}
@Override
public void setPaddingRelative(int start, int top, int end, int bottom) {
super.setPaddingRelative(start, top, end, bottom);
unalignedTopPadding = top;
unalignedBottomPadding = bottom;
if (layout != null) recompute(layout.getWidth());
}
/**
* This is why you don't implement your own TextView kids; you have to handle everything!
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!(text instanceof Spanned)) return super.onTouchEvent(event);
Spannable spannedText = (Spannable) text;
boolean handled = false;
if (event.getAction() == MotionEvent.ACTION_DOWN) {
pressedSpan = getPressedSpan(spannedText, event);
if (pressedSpan != null) {
pressedSpan.setPressed(true);
Selection.setSelection(spannedText, spannedText.getSpanStart(pressedSpan),
spannedText.getSpanEnd(pressedSpan));
handled = true;
postInvalidateOnAnimation();
}
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
TouchableUrlSpan touchedSpan = getPressedSpan(spannedText, event);
if (pressedSpan != null && touchedSpan != pressedSpan) {
pressedSpan.setPressed(false);
pressedSpan = null;
Selection.removeSelection(spannedText);
postInvalidateOnAnimation();
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
if (pressedSpan != null) {
pressedSpan.setPressed(false);
pressedSpan.onClick(this);
handled = true;
postInvalidateOnAnimation();
}
pressedSpan = null;
Selection.removeSelection(spannedText);
} else {
if (pressedSpan != null) {
pressedSpan.setPressed(false);
handled = true;
postInvalidateOnAnimation();
}
pressedSpan = null;
Selection.removeSelection(spannedText);
}
return handled;
}
private TouchableUrlSpan getPressedSpan(Spannable spannable, MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= getPaddingLeft();
y -= getPaddingTop();
x += getScrollX();
y += getScrollY();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
TouchableUrlSpan[] link = spannable.getSpans(off, off, TouchableUrlSpan.class);
TouchableUrlSpan touchedSpan = null;
if (link.length > 0) {
touchedSpan = link[0];
}
return touchedSpan;
}
}