Skip to content

Instantly share code, notes, and snippets.

@homj
Last active April 6, 2017 10:11
Show Gist options
  • Star 60 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save homj/f456dede83cb34a9e997 to your computer and use it in GitHub Desktop.
Save homj/f456dede83cb34a9e997 to your computer and use it in GitHub Desktop.
This Drawable implements the "Drawer-Indicator to Arrow"-Animation as seen in several Material-Design-Apps; NOTE: Mind the updated constructors in Revision 5!
/*
* Copyright 2014 Johannes Homeier
*
* 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 de.twoid.drawable;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.view.Gravity;
public class DrawerIconDrawable extends Drawable {
public static final int STATE_DRAWER = 0;
public static final int STATE_ARROW = 1;
private static final float BASE_DRAWABLE_SIZE = 48f;
private static final float BASE_ICON_SIZE = 18f;
private static final float BASE_BAR_WIDTH = BASE_ICON_SIZE;
private static final float BASE_BAR_HEIGHT = 2f;
private static final float BASE_BAR_SPACING = 5f;
private static final float BASE_BAR_SHRINKAGE = BASE_BAR_WIDTH/6f;
private static final float FULL_ROTATION = 720f;
private static final float TOPRECT_FIRST_ROTATION = 450f;
private static final float TOPRECT_SECOND_ROTATION = FULL_ROTATION-TOPRECT_FIRST_ROTATION;
private static final float MIDRECT_FIRST_ROTATION = 360f;
private static final float MIDRECT_SECOND_ROTATION = FULL_ROTATION-MIDRECT_FIRST_ROTATION;
private static final float BOTRECT_FIRST_ROTATION = 270f;
private static final float BOTRECT_SECOND_ROTATION = FULL_ROTATION-BOTRECT_FIRST_ROTATION;
private static final float LEVEL_BREAKPOINT = 0.5f;
// level of the animation
private float level;
// Dimensions
private int width;
private int height;
private int offsetX;
private int offsetY;
private float barWidth;
private float barHeight;
private float barSpacing;
private float barShrinkage;
// Drawing-Objects
private Paint mPaint;
private Rect iconRect;
private RectF topRect;
private RectF middleRect;
private RectF bottomRect;
private int gravity = Gravity.CENTER;
private boolean breakpointReached = false;
/**
* Create a new DrawerIconDrawableV1 with size in pixel
*
* @param size the size (both width and height) this drawable should have in pixel
*/
public DrawerIconDrawable(int size) {
this(size, size);
}
/**
* Create a new DrawerIconDrawableV1 with a specfied width and height in pixel
* @param width the width this drawable should have in pixel
* @param height the height this drawable should have in pixel
*/
public DrawerIconDrawable(int width, int height) {
this(width, height, 0, 0);
}
/**
* Create a new DrawerIconDrawableV1 with specified width and height in pixel and also apply a {@link Gravity} to align the icon
* @param width the width this drawable should have in pixel
* @param height the height this drawable should have in pixel
* @param gravity the gravity to align the icon in this drawable
*/
public DrawerIconDrawable(int width, int height, int gravity) {
this(width, height, 0, 0, gravity);
}
/**
* Create a new DrawerIconDrawableV1 with specified width and height in pixel and also apply a offset to the icon
* @param width the width this drawable should have in pixel
* @param height the height this drawable should have in pixel
* @param offsetX the offset the icon should have from its center in x-direction
* @param offsetY the offset the icon should have from its center in y-direction
*/
public DrawerIconDrawable(int width, int height, int offsetX, int offsetY) {
this(width, height, 0, 0, Gravity.CENTER);
}
/**
* Create a new DrawerIconDrawableV1 with specified width and height in pixel and also apply a offset to the icon plus a {@link Gravity} to align the icon
* @param width the width this drawable should have in pixel
* @param height the height this drawable should have in pixel
* @param offsetX the offset the icon should have from its center in x-direction
* @param offsetY the offset the icon should have from its center in y-direction
* @param gravity the gravity to align the icon in this drawable
*/
public DrawerIconDrawable(int width, int height, int offsetX, int offsetY, int gravity) {
this.width = width;
this.height = height;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.gravity = gravity;
setBounds(new Rect(0, 0, width, height));
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(0xffffffff);
iconRect = new Rect();
topRect = new RectF();
middleRect = new RectF();
bottomRect = new RectF();
setDefaultIconSize();
setLevel(0);
}
@Override
public void draw(Canvas canvas) {
canvas.translate(iconRect.left + offsetX, iconRect.top + offsetY);
float scaleFactor = level < LEVEL_BREAKPOINT ? level * 2 : (level - 0.5f) * 2;
drawTopRect(canvas, scaleFactor);
drawMiddleRect(canvas, scaleFactor);
drawBottomRect(canvas, scaleFactor);
}
private void drawTopRect(Canvas canvas, float scaleFactor) {
canvas.save();
offsetTopRect(barShrinkage * scaleFactor, 0, -barShrinkage * scaleFactor, 0);
canvas.rotate(
level < LEVEL_BREAKPOINT
? level * TOPRECT_FIRST_ROTATION
: LEVEL_BREAKPOINT*TOPRECT_FIRST_ROTATION + (1 - level) * TOPRECT_SECOND_ROTATION
, iconRect.width()/2
, iconRect.height()/2);
canvas.drawRect(topRect, mPaint);
canvas.restore();
}
private void drawMiddleRect(Canvas canvas, float scaleFactor) {
canvas.save();
offsetMiddleRect(0, 0, -barShrinkage*2f/3f * scaleFactor, 0);
canvas.rotate(
level < LEVEL_BREAKPOINT
? level * MIDRECT_FIRST_ROTATION
: LEVEL_BREAKPOINT*MIDRECT_FIRST_ROTATION + (1 - level) * MIDRECT_SECOND_ROTATION
, iconRect.width()/2
, iconRect.height()/2);
canvas.drawRect(middleRect, mPaint);
canvas.restore();
}
private void drawBottomRect(Canvas canvas, float scaleFactor) {
canvas.save();
offsetBottomRect(barShrinkage * scaleFactor, 0, -barShrinkage * scaleFactor,
0);
canvas.rotate(
level < LEVEL_BREAKPOINT
? level * BOTRECT_FIRST_ROTATION
: LEVEL_BREAKPOINT*BOTRECT_FIRST_ROTATION + (1 - level) * BOTRECT_SECOND_ROTATION
, iconRect.width()/2
, iconRect.height()/2);
canvas.drawRect(bottomRect, mPaint);
canvas.restore();
}
private void offsetTopRect(float offsetLeft, float offsetTop,
float offsetRight, float offsetBottom) {
topRect.set(
iconRect.width()/2 - barWidth/2 + offsetLeft
, iconRect.height()/2 - barSpacing - barHeight/2 + offsetTop
, iconRect.width()/2 + barWidth/2 + offsetRight
, iconRect.height()/2 - barSpacing + barHeight/2 + offsetBottom);
}
private void offsetMiddleRect(float offsetLeft, float offsetTop,
float offsetRight, float offsetBottom) {
middleRect.set(
iconRect.width()/2 - barWidth/2 + offsetLeft
, iconRect.height()/2 - barHeight/2 + offsetTop
, iconRect.width()/2 + barWidth/2 + offsetRight
, iconRect.height()/2 + barHeight/2 + offsetBottom);
}
private void offsetBottomRect(float offsetLeft, float offsetTop,
float offsetRight, float offsetBottom) {
bottomRect.set(
iconRect.width()/2 - barWidth/2 + offsetLeft
, iconRect.height()/2 + barSpacing - barHeight/2 + offsetTop
, iconRect.width()/2 + barWidth/2 + offsetRight
, iconRect.height()/2 + barSpacing + barHeight/2 + offsetBottom);
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
invalidateSelf();
}
@Override
public void setColorFilter(ColorFilter cf) {
mPaint.setColorFilter(cf);
invalidateSelf();
}
@Override
public int getOpacity() {
return 0;
}
/**
* set the color of the Drawable;
* @param color
*/
public void setColor(int color) {
mPaint.setColor(color);
invalidateSelf();
}
/**
* set the size of the icon; this size should be smaller than the size of this drawable itself to fully show the transformation!
* @param size the size of the icon in pixel
*/
public void setIconSize(int size){
if(size > Math.min(width, height)) size = Math.min(width, height);
iconRect.set(0,0,size,size);
Gravity.apply(gravity, iconRect.width(), iconRect.height(), getBounds(), iconRect);
float ratio = size / BASE_ICON_SIZE;
barWidth = BASE_BAR_WIDTH * ratio;
barHeight = BASE_BAR_HEIGHT * ratio;
barSpacing = BASE_BAR_SPACING * ratio;
barShrinkage = BASE_BAR_SHRINKAGE * ratio;
invalidateSelf();
}
/**
* Apply a {@link Gravity} to align the icon in this drawable
* @param gravity the gravity to align the icon in this drawable
*/
public void setGravity(int gravity){
Gravity.apply(gravity, iconRect.width(), iconRect.height(), getBounds(), iconRect);
invalidateSelf();
}
/**
* resets the icon size to its default size (as specified in the Material-Design-guidelines
* this means, the icon will be 1/3 of the minimal size of this drawable
*/
public void setDefaultIconSize(){
setIconSize((int) (Math.min(width, height) * BASE_ICON_SIZE/BASE_DRAWABLE_SIZE));
}
/**
* offset the icon from its center
* @param offsetX the offset the icon should have from its center in x-direction
* @param offsetY the offset the icon should have from its center in y-direction
*/
public void offsetIcon(int offsetX, int offsetY){
this.offsetX = offsetX;
this.offsetY = offsetY;
invalidateSelf();
}
/**
* set the state of the Drawable;
*
* @param level
*/
public void setState(int state) {
switch (state) {
case STATE_DRAWER:
setLevel((float) STATE_DRAWER);
break;
case STATE_ARROW:
setLevel((float) STATE_ARROW);
break;
}
}
/**
* set the level of the animation; drawer indicator is fully displayed at 0;
* arrow is fully displayed at 1
*
* @param level
*/
public void setLevel(float level) {
if (level == 1)
breakpointReached = true;
if (level == 0)
breakpointReached = false;
this.level = (breakpointReached ? LEVEL_BREAKPOINT : 0) + level / 2;
invalidateSelf();
}
@Override
public int getIntrinsicWidth() {
return width;
}
@Override
public int getIntrinsicHeight() {
return height;
}
}
package de.twoid.drawericondrawabletest;
import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
public class MainActivity extends Activity {
DrawerIconDrawable drawerIconDrawable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
float DIPS = getResources().getDisplayMetrics().density;
drawerIconDrawable = new DrawerIconDrawable(
(int) (72*DIPS) //width of the drawable
, (int) (48*DIPS) //height of the drawable
, (int) (18*DIPS) //x-offset of the icon
, 0 //y-offset of the icon
, Gravity.CENTER_VERTICAL | Gravity.LEFT //let the icon center vertically and align to the left of the drawable
);
getActionBar().setDisplayShowHomeEnabled(false);
getActionBar().setDisplayHomeAsUpEnabled(true);
getActionBar().setHomeAsUpIndicator(drawerIconDrawable);
}
}
@castorflex
Copy link

Please, can you rename the file to DrawerIconDrawable.java for the syntax highlighting? Thanks!

@homj
Copy link
Author

homj commented Oct 6, 2014

oops, my fault :P

@Pkmmte
Copy link

Pkmmte commented Oct 12, 2014

This works great, best implementation I've seen so far!

The only issue I see is that it scales too small by default regardless of what size I specify in the constructor parameters. (Looks about 50% small than the size of the play store's icon)
Lowering the scale division fixes it but makes it off-center.

Also, it's missing a way to easily add an x-offset to the right side of the drawable.

@homj
Copy link
Author

homj commented Oct 13, 2014

@Pkmmte thanks! I'm aware of that, yeah... I'm going to post a updated version soon, where you can apply things such as scaling and bar-thickness

@mwinters-stuff
Copy link

Any chance of a example of how to get this into the actionbar as the drawer icon?

@homj
Copy link
Author

homj commented Oct 13, 2014

This should do it:

getActionBar().setHomeAsUpIndiactor(drawerIconDrawable);

@PanHyridae
Copy link

Do I just copy and paste this into the code of my app?

@homj
Copy link
Author

homj commented Oct 15, 2014

Yes, just copy it ;) I'm going to post a updated version soon though.. so you might just wait for that

@Alchete
Copy link

Alchete commented Oct 16, 2014

I must be missing something completely obvious to get this working .. :(

mDrawerIcon = new DrawerIconDrawable(50, 50, 16);
getSupportActionBar().setDisplayShowTitleEnabled(false);
getSupportActionBar().setHomeAsUpIndicator(mDrawerIcon);

Is that all that's needed? When doing this, I only see my application icon in the action bar, not the drawable...

UPDATE:
Ok, I got it working! In case anyone has similar issues, the fix for me was to use:

getSupportActionBar().setLogo(mDrawerIcon);

And, then to achieve the spinning effect, in the ActionBarDrawerToggle::onDrawerSlide() set:
mDrawerIcon.setLevel(slideOffset);

And yes, homj, it sounds like you're aware of the scaling issues. It appears ~2x too small, and the incoming size isn't used.

Looking forward to your next update! And, thank you for providing a wonderfully self-contained "library"!!

@homj
Copy link
Author

homj commented Oct 16, 2014

I just updated the code! You can now set the icon-size besides the drawable-size itself and apply a offset and a gravity to the icon to arrange it.

Feel free to test it and provide some feedback if you notice any bugs or issues!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment