-
Star
(635)
You must be signed in to star a gist -
Fork
(102)
You must be signed in to fork a gist
-
-
Save chrisbanes/91ac8a20acfbdc410a68 to your computer and use it in GitHub Desktop.
<?xml version="1.0" encoding="utf-8"?> | |
<!-- | |
~ 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. | |
--> | |
<resources> | |
<declare-styleable name="CollapsingTitleLayout"> | |
<attr name="expandedMargin" format="reference|dimension" /> | |
<attr name="expandedMarginStart" format="reference|dimension" /> | |
<attr name="expandedMarginBottom" format="reference|dimension" /> | |
<attr name="expandedMarginEnd" format="reference|dimension" /> | |
<attr name="expandedTextSize" format="reference|dimension" /> | |
<attr name="collapsedTextSize" format="reference|dimension" /> | |
<attr name="android:textAppearance" /> | |
<attr name="textSizeInterpolator" format="reference" /> | |
</declare-styleable> | |
<declare-styleable name="CollapsingTextAppearance"> | |
<attr name="android:textSize" /> | |
<attr name="android:textColor" /> | |
</declare-styleable> | |
</resources> |
/* | |
* 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. | |
*/ | |
package your.package; | |
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.support.v7.widget.Toolbar; | |
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.view.animation.AnimationUtils; | |
import android.view.animation.Interpolator; | |
import android.widget.FrameLayout; | |
import app.philm.in.R; | |
public class CollapsingTitleLayout extends FrameLayout { | |
// 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 Toolbar mToolbar; | |
private View mDummyView; | |
private float mScrollOffset; | |
private final Rect mToolbarContentBounds; | |
private float mExpandedMarginLeft; | |
private float mExpandedMarginRight; | |
private float mExpandedMarginBottom; | |
private int mRequestedExpandedTitleTextSize; | |
private int mExpandedTitleTextSize; | |
private int 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; | |
private Interpolator mTextSizeInterpolator; | |
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); | |
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CollapsingTitleLayout); | |
mExpandedMarginLeft = mExpandedMarginRight = mExpandedMarginBottom = | |
a.getDimensionPixelSize(R.styleable.CollapsingTitleLayout_expandedMargin, 0); | |
final boolean isRtl = ViewCompat.getLayoutDirection(this) | |
== ViewCompat.LAYOUT_DIRECTION_RTL; | |
if (a.hasValue(R.styleable.CollapsingTitleLayout_expandedMarginStart)) { | |
final int marginStart = a.getDimensionPixelSize( | |
R.styleable.CollapsingTitleLayout_expandedMarginStart, 0); | |
if (isRtl) { | |
mExpandedMarginRight = marginStart; | |
} else { | |
mExpandedMarginLeft = marginStart; | |
} | |
} | |
if (a.hasValue(R.styleable.CollapsingTitleLayout_expandedMarginEnd)) { | |
final int marginEnd = a.getDimensionPixelSize( | |
R.styleable.CollapsingTitleLayout_expandedMarginEnd, 0); | |
if (isRtl) { | |
mExpandedMarginLeft = marginEnd; | |
} else { | |
mExpandedMarginRight = marginEnd; | |
} | |
} | |
if (a.hasValue(R.styleable.CollapsingTitleLayout_expandedMarginBottom)) { | |
mExpandedMarginBottom = a.getDimensionPixelSize( | |
R.styleable.CollapsingTitleLayout_expandedMarginBottom, 0); | |
} | |
final int tp = a.getResourceId(R.styleable.CollapsingTitleLayout_android_textAppearance, | |
android.R.style.TextAppearance); | |
setTextAppearance(tp); | |
if (a.hasValue(R.styleable.CollapsingTitleLayout_collapsedTextSize)) { | |
mCollapsedTitleTextSize = a.getDimensionPixelSize( | |
R.styleable.CollapsingTitleLayout_collapsedTextSize, 0); | |
} | |
mRequestedExpandedTitleTextSize = a.getDimensionPixelSize( | |
R.styleable.CollapsingTitleLayout_expandedTextSize, mCollapsedTitleTextSize); | |
final int interpolatorId = a | |
.getResourceId(R.styleable.CollapsingTitleLayout_textSizeInterpolator, | |
android.R.anim.accelerate_interpolator); | |
mTextSizeInterpolator = AnimationUtils.loadInterpolator(context, interpolatorId); | |
a.recycle(); | |
mToolbarContentBounds = new Rect(); | |
setWillNotDraw(false); | |
} | |
public void setTextAppearance(int resId) { | |
TypedArray atp = getContext().obtainStyledAttributes(resId, | |
R.styleable.CollapsingTextAppearance); | |
mTextPaint.setColor(atp.getColor( | |
R.styleable.CollapsingTextAppearance_android_textColor, Color.WHITE)); | |
mCollapsedTitleTextSize = atp.getDimensionPixelSize( | |
R.styleable.CollapsingTextAppearance_android_textSize, 0); | |
atp.recycle(); | |
recalculate(); | |
} | |
@Override | |
public void addView(View child, int index, ViewGroup.LayoutParams params) { | |
super.addView(child, index, params); | |
if (child instanceof Toolbar) { | |
mToolbar = (Toolbar) 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. | |
* | |
* 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; | |
final float textSizeOffset = mTextSizeInterpolator != null | |
? mTextSizeInterpolator.getInterpolation(mScrollOffset) | |
: offset; | |
mTextLeft = interpolate(mExpandedMarginLeft, mToolbarContentBounds.left, offset); | |
mTextTop = interpolate(mExpandedTop, mCollapsedTop, offset); | |
mTextRight = interpolate(getWidth() - mExpandedMarginRight, mToolbarContentBounds.right, offset); | |
setInterpolatedTextSize( | |
interpolate(mExpandedTitleTextSize, mCollapsedTitleTextSize, textSizeOffset)); | |
ViewCompat.postInvalidateOnAnimation(this); | |
} | |
private void calculateTextBounds() { | |
final DisplayMetrics metrics = getResources().getDisplayMetrics(); | |
// We then calculate the collapsed text size, using the same logic | |
mTextPaint.setTextSize(mCollapsedTitleTextSize); | |
float textHeight = mTextPaint.descent() - mTextPaint.ascent(); | |
float textOffset = (textHeight / 2) - mTextPaint.descent(); | |
mCollapsedTop = mToolbarContentBounds.centerY() + textOffset; | |
// 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 = (int) Math.max(mCollapsedTitleTextSize, | |
getSingleLineTextSize(mTitle, mTextPaint, | |
getWidth() - mExpandedMarginLeft -mExpandedMarginRight, 0f, | |
mRequestedExpandedTitleTextSize, 0.5f, metrics)); | |
mExpandedTop = getHeight() - mExpandedMarginBottom; | |
// 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; | |
final float ascent = mTextPaint.ascent() * mScale; | |
final float descent = mTextPaint.descent() * mScale; | |
final float h = descent - ascent; | |
if (DEBUG_DRAW) { | |
// Just a debug tool, which drawn a Magneta rect in the text bounds | |
canvas.drawRect(mTextLeft, | |
y - h + descent, | |
mTextRight, | |
y + descent, | |
DEBUG_DRAW_PAINT); | |
} | |
if (mUseTexture) { | |
y = y - h + descent; | |
} | |
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(); | |
} | |
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(); | |
} | |
mUseTexture = USE_SCALING_TEXTURE; | |
} | |
ViewCompat.postInvalidateOnAnimation(this); | |
} | |
private void ensureExpandedTexture() { | |
if (mExpandedTitleTexture != null) return; | |
int w = (int) (getWidth() - mExpandedMarginLeft - mExpandedMarginRight); | |
int h = (int) (mTextPaint.descent() - mTextPaint.ascent()); | |
mExpandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); | |
Canvas c = new Canvas(mExpandedTitleTexture); | |
c.drawText(mTitleToDraw, 0, h - mTextPaint.descent(), 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! | |
recalculate(); | |
} | |
} | |
private void recalculate() { | |
if (getHeight() > 0) { | |
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 | |
recalculate(); | |
} | |
} | |
} | |
private void clearTexture() { | |
if (mExpandedTitleTexture != null) { | |
mExpandedTitleTexture.recycle(); | |
mExpandedTitleTexture = null; | |
} | |
} | |
/** | |
* Recursive binary search to find the best size for the text | |
* | |
* 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); | |
} | |
} |
<?xml version="1.0" encoding="utf-8"?> | |
<!-- | |
~ 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. | |
--> | |
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
android:layout_height="match_parent" | |
android:layout_width="match_parent"> | |
<!-- Your content, maybe a ListView? --> | |
<app.philm.in.view.CollapsingTitleLayout | |
android:id="@+id/backdrop_toolbar" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse" | |
app:expandedTextSize="40dp" | |
app:expandedMargin="16dp"> | |
<android.support.v7.widget.Toolbar | |
android:id="@+id/toolbar" | |
android:layout_height="?attr/actionBarSize" | |
android:layout_width="match_parent" /> | |
</app.philm.in.view.CollapsingTitleLayout> | |
</FrameLayout> |
Cool, thanx!
like!
Wavooo Great
Great work. Thanks.
Great
Thanks!
On this line can you explain what canvas.scale(...)
is doing?
Thanks.
Can someone explain me what happens if I use this with ListView ?
Sweet!
I got an error .. Cant instantiate class ...; no empty constructor .. please help meee . thanksss!!!!
Hi,
im trying use your code but this part of the code not executing, im using android.support.v7.widget.Toolbar, please assist and thanks in advance
if (child instanceof Toolbar) {
mToolbar = (Toolbar) child;
Context cxt = getContext();
mDummyView = new View(cxt);
mToolbar.addView(mDummyView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
Good thing, thank you
It's so cool.
can someone share example code for this ?
@chrisbanes I ran into an issue with the texture not being scaled properly from a collapsed state on SDK_INT < 18. When the texture is collapsed,mTextPaint
's text size is set to mCollapsedTitleTextSize
. The problem occurs when the texture needs to expand from the collapsed state.
In onDraw()
, when mScale
is applied to the ascent and descent, the values of ascent and descent are based on mCollapsedTitleTextSize
when they should instead be based on mExpandedTitleTextSize
.
A quick work around for me was to change the follow from:
final float ascent = mTextPaint.ascent() * mScale;
final float descent = mTextPaint.descent() * mScale;
final float h = descent - ascent;
to:
float textSize = mTextPaint.getTextSize();
mTextPaint.setTextSize(mExpandedTitleTextSize);
final float ascent = mTextPaint.ascent() * mScale;
final float descent = mTextPaint.descent() * mScale;
final float h = descent - ascent;
mTextPaint.setTextSize(textSize);
For the ones that asked for an example, that's my solution. I am using compile 'com.github.ksoichiro:android-observablescrollview:1.5.0'
for the scroll listener.
Example:
@Override
public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
int leftToTop = mCollapsingTitleLayout.getHeight() - scrollY;
if (leftToTop > mCollapsingTitleLayout.getToolbarViewHeight()) {
final float percent = scrollY / (float) (mCollapsingTitleLayout.getHeight() - mCollapsingTitleLayout.getToolbarViewHeight());
mCollapsingTitleLayout.setScrollOffset(percent);
} else {
mCollapsingTitleLayout.setScrollOffset(1.0f);
}
}
I added a new method to the CollapsingTitleLayout
:
public int getToolbarViewHeight() {
return mToolbar.getHeight();
}
And my layout:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/green">
<com.github.ksoichiro.android.observablescrollview.ObservableScrollView
android:id="@+id/scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/header_height"
android:clipToPadding="false">
<FrameLayout
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:text="@string/lipsum" />
</FrameLayout>
</com.github.ksoichiro.android.observablescrollview.ObservableScrollView>
<my.package.utils.CollapsingTitleLayout
android:id="@+id/backdrop_toolbar"
android:layout_width="match_parent"
android:layout_height="@dimen/header_height"
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
app:expandedTextSize="40dp"
app:collapseMarginStart="@dimen/spacing_huge"
app:expandedMargin="@dimen/large">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="@color/green"/>
</my.package.utils.CollapsingTitleLayout>
</FrameLayout>
If you wonder what is app:collapseMarginStart
, it is just a custom attr I added (I need it in my case ) to specify the custom margin on the side.
Minor bug if you add a drawer icon before (shifting the title text location) at any point after initialization:
onLayout:
if (mToolbarContentBounds.left != mDummyView.getLeft()) {
mToolbarContentBounds.left = mDummyView.getLeft();
changed = true;
}
if (mToolbarContentBounds.top != mDummyView.getTop()) {
mToolbarContentBounds.top = mDummyView.getTop();
changed = true;
}
if (mToolbarContentBounds.right != mDummyView.getRight()) {
mToolbarContentBounds.right = mDummyView.getRight();
changed = true;
}
if (mToolbarContentBounds.bottom != mDummyView.getBottom()) {
mToolbarContentBounds.bottom = mDummyView.getBottom();
changed = true;
}
so cool
great design
anyone used this class to create such layout?
Please send the whole project.
Please send sample app code by using this code.
so cool.