Skip to content

Instantly share code, notes, and snippets.

@homj
Last active April 6, 2017 10:11
Show Gist options
  • 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);
}
}
@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