com.cocosw.accessory.views.layout.CollapsingTitleLayout.java Source code

Java tutorial

Introduction

Here is the source code for com.cocosw.accessory.views.layout.CollapsingTitleLayout.java

Source

package com.cocosw.accessory.views.layout;

/*
 * Copyright 2014 Chris Banes
 *
 * 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.
 */

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.support.v4.view.ViewCompat;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import com.cocosw.accessory.view.R;

public class CollapsingTitleLayout extends FrameLayout {

    private static final float DEFAULT_MIN_TEXT_SIZE = 12f; // 12dp

    // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it
    // by using our own texture
    private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18;

    private static final boolean DEBUG_DRAW = false;
    private static final Paint DEBUG_DRAW_PAINT;

    static {
        DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null;
        if (DEBUG_DRAW_PAINT != null) {
            DEBUG_DRAW_PAINT.setAntiAlias(true);
            DEBUG_DRAW_PAINT.setColor(Color.MAGENTA);
        }
    }

    private static final Rect TEMP_RECT = new Rect();

    private ViewGroup mToolbar;
    private View mDummyView;

    private float mScrollOffset;

    private final Rect mToolbarContentBounds;
    private final Rect mTextPaintBounds;
    private final Rect mDrawnTextBounds;

    private float mMinTextSize;

    private float mExpandedMargin;
    private float mRequestedExpandedTitleTextSize;
    private float mExpandedTitleTextSize;
    private float mRequestedCollapsedTitleTextSize;
    private float mCollapsedTitleTextSize;

    private float mExpandedTop;
    private float mCollapsedTop;

    private String mTitle;
    private String mTitleToDraw;
    private boolean mUseTexture;
    private Bitmap mExpandedTitleTexture;

    private float mTextLeft;
    private float mTextRight;
    private float mTextTop;

    private float mScale;

    private final TextPaint mTextPaint;
    private Paint mTexturePaint;

    public CollapsingTitleLayout(Context context) {
        this(context, null);
    }

    public CollapsingTitleLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CollapsingTitleLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mTextPaint = new TextPaint();
        mTextPaint.setAntiAlias(true);

        mMinTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_MIN_TEXT_SIZE,
                getResources().getDisplayMetrics());

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CollapsingTitleLayout);

        mExpandedMargin = a.getDimensionPixelSize(R.styleable.CollapsingTitleLayout_expandedMargin, 0);
        mRequestedExpandedTitleTextSize = a
                .getDimensionPixelSize(R.styleable.CollapsingTitleLayout_expandedTextSize, 0);
        mRequestedCollapsedTitleTextSize = a
                .getDimensionPixelSize(R.styleable.CollapsingTitleLayout_collapsedTextSize, 0);
        mTextPaint.setColor(a.getColor(R.styleable.CollapsingTitleLayout_android_textColor, Color.WHITE));

        final int defaultMinTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                DEFAULT_MIN_TEXT_SIZE, getResources().getDisplayMetrics());
        mMinTextSize = a.getDimensionPixelSize(R.styleable.CollapsingTitleLayout_minTextSize, defaultMinTextSize);

        a.recycle();

        mTextPaintBounds = new Rect();
        mDrawnTextBounds = new Rect();
        mToolbarContentBounds = new Rect();

        setWillNotDraw(false);
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        super.addView(child, index, params);

        if (child.getClass().getName().endsWith(".Toolbar")) {
            mToolbar = (ViewGroup) child;
            mDummyView = new View(getContext());
            mToolbar.addView(mDummyView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        }
    }

    /**
     * Set the value indicating the current scroll value. This decides how much of the
     * background will be displayed, as well as the title metrics/positioning.
     * <p/>
     * A value of {@code 0.0} indicates that the layout is fully expanded.
     * A value of {@code 1.0} indicates that the layout is fully collapsed.
     */
    public void setScrollOffset(float offset) {
        if (offset != mScrollOffset) {
            mScrollOffset = offset;
            calculateOffsets();
        }
    }

    private void calculateOffsets() {
        final float offset = mScrollOffset;

        mTextLeft = interpolate(mExpandedMargin, mToolbarContentBounds.left, offset);
        mTextTop = interpolate(mExpandedTop, mCollapsedTop, offset);
        mTextRight = interpolate(getWidth() - mExpandedMargin, mToolbarContentBounds.right, offset);

        setInterpolatedTextSize(interpolate(mExpandedTitleTextSize, mCollapsedTitleTextSize, offset));

        ViewCompat.postInvalidateOnAnimation(this);
    }

    private void calculateTextBounds() {
        final DisplayMetrics metrics = getResources().getDisplayMetrics();

        // First, let's calculate the expanded text size so that it fit within the bounds
        // We make sure this value is at least our minimum text size
        mExpandedTitleTextSize = Math.max(mMinTextSize, getSingleLineTextSize(mTitle, mTextPaint,
                getWidth() - (mExpandedMargin * 2f), 0f, mRequestedExpandedTitleTextSize, 0.5f, metrics));
        mTextPaint.setTextSize(mExpandedTitleTextSize);
        mTextPaint.getTextBounds(mTitle, 0, mTitle.length(), TEMP_RECT);
        mExpandedTop = getHeight() - TEMP_RECT.height() - mExpandedMargin;

        // We then calculate the collapsed text size, using the same logic
        mCollapsedTitleTextSize = Math.max(mMinTextSize, getSingleLineTextSize(mTitle, mTextPaint,
                mToolbarContentBounds.width(), 0f, mRequestedCollapsedTitleTextSize, 0.5f, metrics));
        mTextPaint.setTextSize(mCollapsedTitleTextSize);
        mTextPaint.getTextBounds(mTitle, 0, mTitle.length(), TEMP_RECT);
        mCollapsedTop = mToolbarContentBounds.centerY() - (TEMP_RECT.height() / 2f);

        // The bounds have changed so we need to clear the texture
        clearTexture();
    }

    @Override
    public void draw(Canvas canvas) {
        final int saveCount = canvas.save();

        final int toolbarHeight = mToolbar.getHeight();
        canvas.clipRect(0, 0, canvas.getWidth(), interpolate(canvas.getHeight(), toolbarHeight, mScrollOffset));

        // Now call super and let it draw the background, etc
        super.draw(canvas);

        if (mTitleToDraw != null) {
            float x = mTextLeft;
            float y = mTextTop;

            if (!mUseTexture) {
                // If we're not drawing a texture, we need to properly offset the text
                x -= mDrawnTextBounds.left;
                y -= mDrawnTextBounds.top;
            }

            if (DEBUG_DRAW) {
                // Just a debug tool, which drawn a Magneta rect in the text bounds
                canvas.drawRect(mTextLeft, mTextTop, mTextRight, mTextTop + mDrawnTextBounds.height(),
                        DEBUG_DRAW_PAINT);
            }

            if (mScale != 1f) {
                canvas.scale(mScale, mScale, x, y);
            }

            if (mUseTexture && mExpandedTitleTexture != null) {
                // If we should use a texture, draw it instead of text
                canvas.drawBitmap(mExpandedTitleTexture, x, y, mTexturePaint);
            } else {
                canvas.drawText(mTitleToDraw, x, y, mTextPaint);
            }
        }

        canvas.restoreToCount(saveCount);
    }

    private void setInterpolatedTextSize(final float textSize) {
        if (mTitle == null)
            return;

        if (isClose(textSize, mCollapsedTitleTextSize) || isClose(textSize, mExpandedTitleTextSize)
                || mTitleToDraw == null) {
            // If the text size is 'close' to being a decimal, then we use this as a sync-point.
            // We disable our manual scaling and set the paint's text size.
            mTextPaint.setTextSize(textSize);
            mScale = 1f;

            // We also use this as an opportunity to ellipsize the string
            final CharSequence title = TextUtils.ellipsize(mTitle, mTextPaint, (mTextRight - mTextLeft),
                    TextUtils.TruncateAt.END);
            if (title != mTitleToDraw) {
                // If the title has changed, turn it into a string
                mTitleToDraw = title.toString();
            }

            // As we've changed the text size (and possibly the text) we'll re-measure the text
            mTextPaint.getTextBounds(mTitleToDraw, 0, mTitleToDraw.length(), mTextPaintBounds);
            mDrawnTextBounds.set(mTextPaintBounds);

            if (USE_SCALING_TEXTURE && isClose(textSize, mExpandedTitleTextSize)) {
                ensureExpandedTexture();
            }
            mUseTexture = false;
        } else {
            // We're not close to a decimal so use our canvas scaling method
            if (mExpandedTitleTexture != null) {
                mScale = textSize / mExpandedTitleTextSize;
            } else {
                mScale = textSize / mTextPaint.getTextSize();
            }

            // Because we're scaling using canvas, we need to update the drawn text bounds too
            mDrawnTextBounds.set(mTextPaintBounds);
            mDrawnTextBounds.left *= mScale;
            mDrawnTextBounds.top *= mScale;
            mDrawnTextBounds.right *= mScale;
            mDrawnTextBounds.bottom *= mScale;

            mUseTexture = USE_SCALING_TEXTURE;
        }

        ViewCompat.postInvalidateOnAnimation(this);
    }

    private void ensureExpandedTexture() {
        if (mExpandedTitleTexture != null)
            return;

        mExpandedTitleTexture = Bitmap.createBitmap(mTextPaintBounds.width(), mTextPaintBounds.height(),
                Bitmap.Config.ARGB_8888);

        Canvas c = new Canvas(mExpandedTitleTexture);
        c.drawText(mTitleToDraw, -mTextPaintBounds.left, -mTextPaintBounds.top, mTextPaint);

        if (mTexturePaint == null) {
            // Make sure we have a paint
            mTexturePaint = new Paint();
            mTexturePaint.setAntiAlias(true);
            mTexturePaint.setFilterBitmap(true);
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        mToolbarContentBounds.left = mDummyView.getLeft();
        mToolbarContentBounds.top = mDummyView.getTop();
        mToolbarContentBounds.right = mDummyView.getRight();
        mToolbarContentBounds.bottom = mDummyView.getBottom();

        if (changed && mTitle != null) {
            // If we've changed and we have a title, re-calculate everything!
            calculateTextBounds();
            calculateOffsets();
        }
    }

    /**
     * Set the title to display
     *
     * @param title
     */
    public void setTitle(String title) {
        if (title == null || !title.equals(mTitle)) {
            mTitle = title;

            clearTexture();

            if (getHeight() > 0) {
                // If we've already been laid out, calculate everything now otherwise we'll wait
                // until a layout
                calculateTextBounds();
                calculateOffsets();
            }
        }
    }

    private void clearTexture() {
        if (mExpandedTitleTexture != null) {
            mExpandedTitleTexture.recycle();
            mExpandedTitleTexture = null;
        }
    }

    /**
     * Recursive binary search to find the best size for the text
     * <p/>
     * Adapted from https://github.com/grantland/android-autofittextview
     */
    private static float getSingleLineTextSize(String text, TextPaint paint, float targetWidth, float low,
            float high, float precision, DisplayMetrics metrics) {
        final float mid = (low + high) / 2.0f;

        paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, mid, metrics));
        final float maxLineWidth = paint.measureText(text);

        if ((high - low) < precision) {
            return low;
        } else if (maxLineWidth > targetWidth) {
            return getSingleLineTextSize(text, paint, targetWidth, low, mid, precision, metrics);
        } else if (maxLineWidth < targetWidth) {
            return getSingleLineTextSize(text, paint, targetWidth, mid, high, precision, metrics);
        } else {
            return mid;
        }
    }

    /**
     * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently
     * defined as it's difference being < 0.01.
     */
    private static boolean isClose(float value, float targetValue) {
        return Math.abs(value - targetValue) < 0.01f;
    }

    /**
     * Interpolate between {@code startValue} and {@code endValue}, using {@code progress}.
     */
    private static float interpolate(float startValue, float endValue, float progress) {
        return startValue + ((endValue - startValue) * progress);
    }
}