package org.commcare.views; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Shader; import android.graphics.Shader.TileMode; import android.view.View; import android.view.animation.Animation; import android.view.animation.Transformation; import android.widget.TextView; /** * @author ctsims */ public class ShrinkingTextView extends TextView { private boolean mExpanded = false; private boolean mInteractive = true; private static final int mAnimationDuration = 500; private ExpandAnimation mCurrentAnimation; private int mFullHeight = -1; private int maxHeightCalculated; private int maxHeightPassedIn = -1; private int mAnimatingHeight = -1; public ShrinkingTextView(Context context, int max) { super(context); this.setOnClickListener(new PanelToggler()); updateMaxHeight(max); } public void updateMaxHeight(int maxHeight) { int paddingHeight = this.getPaddingBottom() + this.getPaddingTop(); float textSize = this.getTextSize(); float lineHeight = this.getLineHeight(); float stH = lineHeight + paddingHeight; maxHeightPassedIn = maxHeight; if (maxHeight * 1f < stH) { // Don't allow this control to be smaller than one line maxHeightCalculated = (int)stH; } else { //Otherwise, count up lines until this will spill over, then //set the max height to be the bottom of the lowest line int contentHeightAllottedByGivenMax = maxHeight - paddingHeight; float countUp = textSize; while (countUp + lineHeight < contentHeightAllottedByGivenMax) { countUp = countUp + lineHeight; } // We're now on the last full line. We either want to put our baseline at the bottom // of this line, or the bottom of the next if (countUp + textSize < contentHeightAllottedByGivenMax) { countUp = countUp + textSize; } maxHeightCalculated = (int)(countUp + paddingHeight); } //TODO: Don't mess with this during animation? //this.mMaxHeight = maxHeight; resetDynamicLayout(); //TODO: request layout _on change_? this.requestLayout(); } @Override public void draw(Canvas canvas) { super.draw(canvas); if (mInteractive && !mExpanded || isAnimating()) { Rect r = new Rect(); this.getDrawingRect(r); Paint p = new Paint(); float f = this.getTextSize(); int px = (int)(f / 2); int bottom = r.bottom - this.getPaddingBottom(); int top = bottom - px; int start = Color.argb(0, 255, 255, 255); int end = Color.argb(220, 255, 255, 255); Shader shader = new LinearGradient(r.left, top, r.left, bottom, start, end, TileMode.CLAMP); p.setShader(shader); r.set(r.left, top, r.right, bottom); p.setStyle(Paint.Style.FILL); canvas.drawRect(r, p); } } @Override public void setText(CharSequence text, BufferType type) { super.setText(text, type); this.resetDynamicLayout(); } private void resetDynamicLayout() { //don't manipulate the expanded flag. mInteractive = true; mFullHeight = -1; if (maxHeightCalculated == -1) { mInteractive = false; mExpanded = false; } } private class PanelToggler implements OnClickListener { @Override public void onClick(View v) { if (!mInteractive || isAnimating()) { return; } if (mExpanded) { if (mFullHeight == -1) { mFullHeight = ShrinkingTextView.this.getMeasuredHeight(); } mCurrentAnimation = new ExpandAnimation(mFullHeight, maxHeightCalculated); } else { mCurrentAnimation = new ExpandAnimation(maxHeightCalculated, mFullHeight); } mCurrentAnimation.setDuration(mAnimationDuration); ShrinkingTextView.this.startAnimation(mCurrentAnimation); mExpanded = !mExpanded; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (isAnimating()) { this.setMeasuredDimension(this.getMeasuredWidth(), mAnimatingHeight); } if (mExpanded || !mInteractive || isAnimating()) { return; } //We're in a collapsed state we think is interactive. First, //measure the total height mFullHeight = mFullHeight == -1 ? this.getMeasuredHeight() : mFullHeight; //The full height here isn't greater than the max, //no need for heroics if (mFullHeight <= maxHeightPassedIn) { mInteractive = false; return; } int measuredWidth = this.getMeasuredWidth(); this.setMeasuredDimension(measuredWidth, maxHeightCalculated); } private boolean isAnimating() { return !(mCurrentAnimation == null || mCurrentAnimation.hasEnded() || !mCurrentAnimation.hasStarted()); } private class ExpandAnimation extends Animation { private final int mStartHeight; private final int mDeltaHeight; public ExpandAnimation(int startHeight, int endHeight) { mStartHeight = startHeight; mDeltaHeight = endHeight - startHeight; } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { //ShrinkingTextView.this.setHeight((int) (mStartHeight + mDeltaHeight * interpolatedTime)); mAnimatingHeight = (int)(mStartHeight + mDeltaHeight * interpolatedTime); ShrinkingTextView.this.requestLayout(); } @Override public boolean willChangeBounds() { return true; } } }