Project import
diff --git a/MenuDrawer/AndroidManifest.xml b/MenuDrawer/AndroidManifest.xml
new file mode 100755
index 0000000..a77d28a
--- /dev/null
+++ b/MenuDrawer/AndroidManifest.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="net.simonvt.menudrawer"
+      android:versionCode="3"
+      android:versionName="2.0.1">
+
+    <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="16" />
+</manifest>
diff --git a/MenuDrawer/atlassian-ide-plugin.xml b/MenuDrawer/atlassian-ide-plugin.xml
new file mode 100644
index 0000000..c7826a8
--- /dev/null
+++ b/MenuDrawer/atlassian-ide-plugin.xml
@@ -0,0 +1,5 @@
+<atlassian-ide-plugin>

+  <project-configuration>

+    <servers />

+  </project-configuration>

+</atlassian-ide-plugin>
\ No newline at end of file
diff --git a/MenuDrawer/build.xml b/MenuDrawer/build.xml
new file mode 100755
index 0000000..e5eb1dc
--- /dev/null
+++ b/MenuDrawer/build.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="menudrawer" default="help">
+
+    <!-- The local.properties file is created and updated by the 'android' tool.
+         It contains the path to the SDK. It should *NOT* be checked into
+         Version Control Systems. -->
+    <property file="local.properties" />
+
+    <!-- The ant.properties file can be created by you. It is only edited by the
+         'android' tool to add properties to it.
+         This is the place to change some Ant specific build properties.
+         Here are some properties you may want to change/update:
+
+         source.dir
+             The name of the source directory. Default is 'src'.
+         out.dir
+             The name of the output directory. Default is 'bin'.
+
+         For other overridable properties, look at the beginning of the rules
+         files in the SDK, at tools/ant/build.xml
+
+         Properties related to the SDK location or the project target should
+         be updated using the 'android' tool with the 'update' action.
+
+         This file is an integral part of the build system for your
+         application and should be checked into Version Control Systems.
+
+         -->
+    <property file="ant.properties" />
+
+    <!-- if sdk.dir was not set from one of the property file, then
+         get it from the ANDROID_HOME env var.
+         This must be done before we load project.properties since
+         the proguard config can use sdk.dir -->
+    <property environment="env" />
+    <condition property="sdk.dir" value="${env.ANDROID_HOME}">
+        <isset property="env.ANDROID_HOME" />
+    </condition>
+
+    <!-- The project.properties file is created and updated by the 'android'
+         tool, as well as ADT.
+
+         This contains project specific properties such as project target, and library
+         dependencies. Lower level build properties are stored in ant.properties
+         (or in .classpath for Eclipse projects).
+
+         This file is an integral part of the build system for your
+         application and should be checked into Version Control Systems. -->
+    <loadproperties srcFile="project.properties" />
+
+    <!-- quick check on sdk.dir -->
+    <fail
+            message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
+            unless="sdk.dir"
+    />
+
+    <!--
+        Import per project custom build rules if present at the root of the project.
+        This is the place to put custom intermediary targets such as:
+            -pre-build
+            -pre-compile
+            -post-compile (This is typically used for code obfuscation.
+                           Compiled code location: ${out.classes.absolute.dir}
+                           If this is not done in place, override ${out.dex.input.absolute.dir})
+            -post-package
+            -post-build
+            -pre-clean
+    -->
+    <import file="custom_rules.xml" optional="true" />
+
+    <!-- Import the actual build file.
+
+         To customize existing targets, there are two options:
+         - Customize only one target:
+             - copy/paste the target into this file, *before* the
+               <import> task.
+             - customize it to your needs.
+         - Customize the whole content of build.xml
+             - copy/paste the content of the rules files (minus the top node)
+               into this file, replacing the <import> task.
+             - customize to your needs.
+
+         ***********************
+         ****** IMPORTANT ******
+         ***********************
+         In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
+         in order to avoid having your file be overridden by tools such as "android update project"
+    -->
+    <!-- version-tag: 1 -->
+    <import file="${sdk.dir}/tools/ant/build.xml" />
+
+</project>
diff --git a/MenuDrawer/pom.xml b/MenuDrawer/pom.xml
new file mode 100755
index 0000000..b5bd881
--- /dev/null
+++ b/MenuDrawer/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>net.simonvt</groupId>
+        <artifactId>android-menudrawer-parent</artifactId>
+        <version>2.0.3-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>android-menudrawer</artifactId>
+    <name>Android MenuDrawer</name>
+    <packaging>apklib</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.android</groupId>
+            <artifactId>android</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <sourceDirectory>src</sourceDirectory>
+
+        <plugins>
+            <plugin>
+                <groupId>com.jayway.maven.plugins.android.generation2</groupId>
+                <artifactId>android-maven-plugin</artifactId>
+                <extensions>true</extensions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/MenuDrawer/project.properties b/MenuDrawer/project.properties
new file mode 100755
index 0000000..87089b8
--- /dev/null
+++ b/MenuDrawer/project.properties
@@ -0,0 +1,16 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+android.library=true
+# Project target.
+target=android-16
+
diff --git a/MenuDrawer/res/values/attrs.xml b/MenuDrawer/res/values/attrs.xml
new file mode 100755
index 0000000..c8ec779
--- /dev/null
+++ b/MenuDrawer/res/values/attrs.xml
@@ -0,0 +1,43 @@
+<resources>
+
+    <!-- Reference to a style for the menu drawer. -->
+    <attr name="menuDrawerStyle" format="reference" />
+
+    <!-- Styleables used for styling the menu drawer. -->
+    <declare-styleable name="MenuDrawer">
+
+        <!-- Drawable to use for the background of the content. -->
+        <attr name="mdContentBackground" format="reference" />
+
+        <!-- Drawable to use for the background of the menu. -->
+        <attr name="mdMenuBackground" format="reference" />
+
+        <!-- The size of the menu. -->
+        <attr name="mdMenuSize" format="dimension" />
+
+        <!-- Drawable used as indicator for the active view. -->
+        <attr name="mdActiveIndicator" format="reference" />
+
+        <!-- Defines whether the content will have a dropshadow onto the menu. Default is true. -->
+        <attr name="mdDropShadowEnabled" format="boolean" />
+
+        <!-- The size of the drop shadow. Default is 6dp -->
+        <attr name="mdDropShadowSize" format="dimension" />
+
+        <!-- The color of the drop shadow. Default is #FF000000. -->
+        <attr name="mdDropShadowColor" format="color" />
+
+        <!-- Drawable used for the drop shadow. -->
+        <attr name="mdDropShadow" format="reference" />
+
+        <!-- The touch bezel size. -->
+        <attr name="mdTouchBezelSize" format="dimension" />
+
+        <!-- Whether the indicator should be animated between active views. -->
+        <attr name="mdAllowIndicatorAnimation" format="boolean" />
+
+        <!-- The maximum animation duration -->
+        <attr name="mdMaxAnimationDuration" format="integer" />
+    </declare-styleable>
+
+</resources>
diff --git a/MenuDrawer/res/values/colors.xml b/MenuDrawer/res/values/colors.xml
new file mode 100755
index 0000000..2866c6a
--- /dev/null
+++ b/MenuDrawer/res/values/colors.xml
@@ -0,0 +1,6 @@
+<resources>
+
+    <!-- The default background of the menu. -->
+    <color name="md__defaultBackground">#FF555555</color>
+
+</resources>
diff --git a/MenuDrawer/res/values/ids.xml b/MenuDrawer/res/values/ids.xml
new file mode 100755
index 0000000..c600191
--- /dev/null
+++ b/MenuDrawer/res/values/ids.xml
@@ -0,0 +1,20 @@
+<resources>
+
+    <!-- ID used when defining the content layout in XML. -->
+    <item name="mdContent" type="id" />
+
+    <!-- ID used when defining the menu layout in XML. -->
+    <item name="mdMenu" type="id" />
+
+    <!-- The ID of the content container. -->
+    <item name="md__content" type="id" />
+
+    <!-- The ID of the menu container. -->
+    <item name="md__menu" type="id" />
+
+    <!-- The ID of the drawer. -->
+    <item name="md__drawer" type="id" />
+
+    <!-- Used with View#setTag(int) to specify a position for the active view. -->
+    <item name="mdActiveViewPosition" type="id" />
+</resources>
diff --git a/MenuDrawer/res/values/styles.xml b/MenuDrawer/res/values/styles.xml
new file mode 100755
index 0000000..415be6d
--- /dev/null
+++ b/MenuDrawer/res/values/styles.xml
@@ -0,0 +1,11 @@
+<resources>
+
+    <style name="Widget" />
+
+    <!-- Base theme for the menu drawer. -->
+    <style name="Widget.MenuDrawer">
+        <item name="mdMenuBackground">@color/md__defaultBackground</item>
+        <item name="mdContentBackground">?android:attr/windowBackground</item>
+    </style>
+
+</resources>
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/BottomDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/BottomDrawer.java
new file mode 100755
index 0000000..7a2f7ac
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/BottomDrawer.java
@@ -0,0 +1,240 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+public class BottomDrawer extends VerticalDrawer {
+
+    private int mIndicatorLeft;
+
+    BottomDrawer(Activity activity, int dragMode) {
+        super(activity, dragMode);
+    }
+
+    public BottomDrawer(Context context) {
+        super(context);
+    }
+
+    public BottomDrawer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public BottomDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void openMenu(boolean animate) {
+        animateOffsetTo(-mMenuSize, 0, animate);
+    }
+
+    @Override
+    public void closeMenu(boolean animate) {
+        animateOffsetTo(0, 0, animate);
+    }
+
+    @Override
+    public void setDropShadowColor(int color) {
+        final int endColor = color & 0x00FFFFFF;
+        mDropShadowDrawable = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
+                new int[] {
+                        color,
+                        endColor,
+                });
+        invalidate();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int width = r - l;
+        final int height = b - t;
+        final int offsetPixels = (int) mOffsetPixels;
+        final int menuSize = mMenuSize;
+
+        mMenuContainer.layout(0, height - menuSize, width, height);
+        offsetMenu(offsetPixels);
+
+        if (USE_TRANSLATIONS) {
+            mContentContainer.layout(0, 0, width, height);
+        } else {
+            mContentContainer.layout(0, offsetPixels, width, height + offsetPixels);
+        }
+    }
+
+    /**
+     * Offsets the menu relative to its original position based on the position of the content.
+     *
+     * @param offsetPixels The number of pixels the content if offset.
+     */
+    private void offsetMenu(int offsetPixels) {
+        if (mOffsetMenu && mMenuSize != 0) {
+            final int height = getHeight();
+            final int menuSize = mMenuSize;
+            final float openRatio = (menuSize + (float) offsetPixels) / menuSize;
+
+            if (USE_TRANSLATIONS) {
+                if (offsetPixels != 0) {
+                    final int offset = (int) (0.25f * (openRatio * menuSize));
+                    mMenuContainer.setTranslationY(offset);
+                } else {
+                    mMenuContainer.setTranslationY(height + menuSize);
+                }
+
+            } else {
+                final int oldMenuTop = mMenuContainer.getTop();
+                final int offsetBy = (int) (0.25f * (openRatio * menuSize));
+                final int offset = height - mMenuSize + offsetBy - oldMenuTop;
+                mMenuContainer.offsetTopAndBottom(offset);
+                mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
+            }
+        }
+    }
+
+    @Override
+    protected void drawDropShadow(Canvas canvas, int offsetPixels) {
+        final int width = getWidth();
+        final int height = getHeight();
+
+        mDropShadowDrawable.setBounds(0, height + offsetPixels, width, height + offsetPixels + mDropShadowSize);
+        mDropShadowDrawable.draw(canvas);
+    }
+
+    @Override
+    protected void drawMenuOverlay(Canvas canvas, int offsetPixels) {
+        final int width = getWidth();
+        final int height = getHeight();
+        final float openRatio = ((float) Math.abs(offsetPixels)) / mMenuSize;
+
+        mMenuOverlay.setBounds(0, height + offsetPixels, width, height);
+        mMenuOverlay.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * (1.f - openRatio)));
+        mMenuOverlay.draw(canvas);
+    }
+
+    @Override
+    protected void drawWindowEdge (Canvas canvas, int offsetPixels) {
+        final int width = getWidth();
+        final int height = getHeight();
+        final int edgeHeight = mWindowEdge.getIntrinsicHeight();
+        final int top = height + offsetPixels + edgeHeight;
+        final int bottom = height + offsetPixels;
+        final float openRatio = ((float) Math.abs(offsetPixels)) / mMenuSize;
+
+        mWindowEdge.setBounds(0, top, width, bottom);
+        mWindowEdge.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * (openRatio)));
+        mWindowEdge.draw(canvas);
+    }
+
+    @Override
+    protected void drawIndicator(Canvas canvas, int offsetPixels) {
+        if (mActiveView != null && isViewDescendant(mActiveView)) {
+            Integer position = (Integer) mActiveView.getTag(R.id.mdActiveViewPosition);
+            final int pos = position == null ? 0 : position;
+
+            if (pos == mActivePosition) {
+                final int height = getHeight();
+                final int menuHeight = mMenuSize;
+                final int indicatorHeight = mActiveIndicator.getHeight();
+
+                final float openRatio = ((float) Math.abs(offsetPixels)) / menuHeight;
+
+                mActiveView.getDrawingRect(mActiveRect);
+                offsetDescendantRectToMyCoords(mActiveView, mActiveRect);
+                final int indicatorWidth = mActiveIndicator.getWidth();
+
+                final float interpolatedRatio = 1.f - INDICATOR_INTERPOLATOR.getInterpolation((1.f - openRatio));
+                final int interpolatedHeight = (int) (indicatorHeight * interpolatedRatio);
+
+                final int indicatorBottom = height + offsetPixels + interpolatedHeight;
+                final int indicatorTop = indicatorBottom - indicatorHeight;
+                if (mIndicatorAnimating) {
+                    final int finalLeft = mActiveRect.left + ((mActiveRect.width() - indicatorWidth) / 2);
+                    final int startLeft = mIndicatorStartPos;
+                    final int diff = finalLeft - startLeft;
+                    final int startOffset = (int) (diff * mIndicatorOffset);
+                    mIndicatorLeft = startLeft + startOffset;
+                } else {
+                    mIndicatorLeft = mActiveRect.left + ((mActiveRect.width() - indicatorWidth) / 2);
+                }
+
+                canvas.save();
+                canvas.clipRect(mIndicatorLeft, height + offsetPixels, mIndicatorLeft + indicatorWidth,
+                        indicatorBottom);
+                canvas.drawBitmap(mActiveIndicator, mIndicatorLeft, indicatorTop, null);
+                canvas.restore();
+            }
+        }
+    }
+
+    @Override
+    protected int getIndicatorStartPos() {
+        return mIndicatorLeft;
+    }
+
+    @Override
+    protected void initPeekScroller() {
+        final int dx = -mMenuSize / 3;
+        mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
+    }
+
+    @Override
+    protected void onOffsetPixelsChanged(int offsetPixels) {
+        if (USE_TRANSLATIONS) {
+            mContentContainer.setTranslationY(offsetPixels);
+            offsetMenu(offsetPixels);
+            invalidate();
+        } else {
+            mContentContainer.offsetTopAndBottom(offsetPixels - mContentContainer.getTop());
+            offsetMenu(offsetPixels);
+            invalidate();
+        }
+    }
+
+    //////////////////////////////////////////////////////////////////////
+    // Touch handling
+    //////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected boolean isContentTouch(MotionEvent ev) {
+        return ev.getY() < getHeight() + mOffsetPixels;
+    }
+
+    @Override
+    protected boolean onDownAllowDrag(MotionEvent ev) {
+        final int height = getHeight();
+        return (!mMenuVisible && mInitialMotionY >= height - mTouchSize)
+                || (mMenuVisible && mInitialMotionY <= height + mOffsetPixels);
+    }
+
+    @Override
+    protected boolean onMoveAllowDrag(MotionEvent ev, float diff) {
+        final int height = getHeight();
+        return (!mMenuVisible && mInitialMotionY >= height - mTouchSize && (diff < 0))
+                || (mMenuVisible && mInitialMotionY <= height + mOffsetPixels);
+    }
+
+    @Override
+    protected void onMoveEvent(float dx) {
+        setOffsetPixels(Math.max(Math.min(mOffsetPixels + dx, 0), -mMenuSize));
+    }
+
+    @Override
+    protected void onUpEvent(MotionEvent ev, boolean isDownOnOutside) {
+        final int offsetPixels = (int) mOffsetPixels;
+
+        if (mIsDragging) {
+            mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+            final int initialVelocity = (int) mVelocityTracker.getXVelocity();
+            mLastMotionY = ev.getY();
+            animateOffsetTo(mVelocityTracker.getYVelocity() < 0 ? -mMenuSize : 0, initialVelocity,
+                    true);
+
+            // Close the menu when content is clicked while the menu is visible.
+        } else if (mMenuVisible && ev.getY() < getHeight() + offsetPixels) {
+            closeMenu();
+        }
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/BottomStaticDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/BottomStaticDrawer.java
new file mode 100755
index 0000000..df4b5f4
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/BottomStaticDrawer.java
@@ -0,0 +1,85 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+
+public class BottomStaticDrawer extends StaticDrawer {
+
+    private int mIndicatorLeft;
+
+    BottomStaticDrawer(Activity activity, int dragMode) {
+        super(activity, dragMode);
+    }
+
+    public BottomStaticDrawer(Context context) {
+        super(context);
+    }
+
+    public BottomStaticDrawer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public BottomStaticDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void initDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super.initDrawer(context, attrs, defStyle);
+        mPosition = Position.BOTTOM;
+    }
+
+    @Override
+    public void setDropShadowColor(int color) {
+        final int endColor = color & 0x00FFFFFF;
+        mDropShadowDrawable = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[] {
+                color,
+                endColor,
+        });
+        invalidate();
+    }
+
+    @Override
+    protected void drawIndicator(Canvas canvas) {
+        if (mActiveView != null && isViewDescendant(mActiveView)) {
+            Integer position = (Integer) mActiveView.getTag(R.id.mdActiveViewPosition);
+            final int pos = position == null ? 0 : position;
+
+            if (pos == mActivePosition) {
+                final int height = getHeight();
+                final int menuHeight = mMenuSize;
+                final int indicatorHeight = mActiveIndicator.getHeight();
+
+                mActiveView.getDrawingRect(mActiveRect);
+                offsetDescendantRectToMyCoords(mActiveView, mActiveRect);
+                final int indicatorWidth = mActiveIndicator.getWidth();
+
+                final int indicatorTop = height - menuHeight;
+                final int indicatorBottom = indicatorTop + indicatorHeight;
+                if (mIndicatorAnimating) {
+                    final int finalLeft = mActiveRect.left + ((mActiveRect.width() - indicatorWidth) / 2);
+                    final int startLeft = mIndicatorStartPos;
+                    final int diff = finalLeft - startLeft;
+                    final int startOffset = (int) (diff * mIndicatorOffset);
+                    mIndicatorLeft = startLeft + startOffset;
+                } else {
+                    mIndicatorLeft = mActiveRect.left + ((mActiveRect.width() - indicatorWidth) / 2);
+                }
+
+                canvas.save();
+                canvas.clipRect(mIndicatorLeft, indicatorTop, mIndicatorLeft + indicatorWidth,
+                        indicatorBottom);
+                canvas.drawBitmap(mActiveIndicator, mIndicatorLeft, indicatorTop, null);
+                canvas.restore();
+            }
+        }
+    }
+
+    @Override
+    protected int getIndicatorStartPos() {
+        return mIndicatorLeft;
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/BuildLayerFrameLayout.java b/MenuDrawer/src/net/simonvt/menudrawer/BuildLayerFrameLayout.java
new file mode 100755
index 0000000..45f5aa1
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/BuildLayerFrameLayout.java
@@ -0,0 +1,99 @@
+package net.simonvt.menudrawer;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+/**
+ * FrameLayout which caches the hardware layer if available.
+ * <p/>
+ * If it's not posted twice the layer either wont be built on start, or it'll be built twice.
+ */
+public class BuildLayerFrameLayout extends FrameLayout {
+
+    private boolean mChanged;
+
+    private boolean mHardwareLayersEnabled = true;
+
+    private boolean mAttached;
+
+    private boolean mFirst = true;
+
+    public BuildLayerFrameLayout(Context context) {
+        super(context);
+        if (MenuDrawer.USE_TRANSLATIONS) {
+            setLayerType(LAYER_TYPE_HARDWARE, null);
+        }
+    }
+
+    public BuildLayerFrameLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        if (MenuDrawer.USE_TRANSLATIONS) {
+            setLayerType(LAYER_TYPE_HARDWARE, null);
+        }
+    }
+
+    public BuildLayerFrameLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        if (MenuDrawer.USE_TRANSLATIONS) {
+            setLayerType(LAYER_TYPE_HARDWARE, null);
+        }
+    }
+
+    void setHardwareLayersEnabled(boolean enabled) {
+        mHardwareLayersEnabled = enabled;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mAttached = true;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mAttached = false;
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        if (MenuDrawer.USE_TRANSLATIONS && mHardwareLayersEnabled) {
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    mChanged = true;
+                    invalidate();
+                }
+            });
+        }
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+
+        if (mChanged && MenuDrawer.USE_TRANSLATIONS) {
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    if (mAttached) {
+                        final int layerType = getLayerType();
+                        // If it's already a hardware layer, it'll be built anyway.
+                        if (layerType != LAYER_TYPE_HARDWARE || mFirst) {
+                            mFirst = false;
+                            setLayerType(LAYER_TYPE_HARDWARE, null);
+                            buildLayer();
+                            setLayerType(LAYER_TYPE_NONE, null);
+                        }
+                    }
+                }
+            });
+
+            mChanged = false;
+        }
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/ColorDrawable.java b/MenuDrawer/src/net/simonvt/menudrawer/ColorDrawable.java
new file mode 100755
index 0000000..10a35e7
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/ColorDrawable.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * 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 net.simonvt.menudrawer;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+
+/**
+ * A specialized Drawable that fills the Canvas with a specified color.
+ * Note that a ColorDrawable ignores the ColorFilter.
+ * <p/>
+ * <p>It can be defined in an XML file with the <code>&lt;color></code> element.</p>
+ *
+ * @attr ref android.R.styleable#ColorDrawable_color
+ */
+public class ColorDrawable extends Drawable {
+
+    private ColorState mState;
+    private final Paint mPaint = new Paint();
+
+    /** Creates a new black ColorDrawable. */
+    public ColorDrawable() {
+        this(null);
+    }
+
+    /**
+     * Creates a new ColorDrawable with the specified color.
+     *
+     * @param color The color to draw.
+     */
+    public ColorDrawable(int color) {
+        this(null);
+        setColor(color);
+    }
+
+    private ColorDrawable(ColorState state) {
+        mState = new ColorState(state);
+    }
+
+    @Override
+    public int getChangingConfigurations() {
+        return super.getChangingConfigurations() | mState.mChangingConfigurations;
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        if ((mState.mUseColor >>> 24) != 0) {
+            mPaint.setColor(mState.mUseColor);
+            canvas.drawRect(getBounds(), mPaint);
+        }
+    }
+
+    /**
+     * Gets the drawable's color value.
+     *
+     * @return int The color to draw.
+     */
+    public int getColor() {
+        return mState.mUseColor;
+    }
+
+    /**
+     * Sets the drawable's color value. This action will clobber the results of prior calls to
+     * {@link #setAlpha(int)} on this object, which side-affected the underlying color.
+     *
+     * @param color The color to draw.
+     */
+    public void setColor(int color) {
+        if (mState.mBaseColor != color || mState.mUseColor != color) {
+            invalidateSelf();
+            mState.mBaseColor = mState.mUseColor = color;
+        }
+    }
+
+    /**
+     * Returns the alpha value of this drawable's color.
+     *
+     * @return A value between 0 and 255.
+     */
+    public int getAlpha() {
+        return mState.mUseColor >>> 24;
+    }
+
+    /**
+     * Sets the color's alpha value.
+     *
+     * @param alpha The alpha value to set, between 0 and 255.
+     */
+    public void setAlpha(int alpha) {
+        alpha += alpha >> 7;   // make it 0..256
+        int baseAlpha = mState.mBaseColor >>> 24;
+        int useAlpha = baseAlpha * alpha >> 8;
+        int oldUseColor = mState.mUseColor;
+        mState.mUseColor = (mState.mBaseColor << 8 >>> 8) | (useAlpha << 24);
+        if (oldUseColor != mState.mUseColor) {
+            invalidateSelf();
+        }
+    }
+
+    /**
+     * Setting a color filter on a ColorDrawable has no effect.
+     *
+     * @param colorFilter Ignore.
+     */
+    public void setColorFilter(ColorFilter colorFilter) {
+    }
+
+    public int getOpacity() {
+        switch (mState.mUseColor >>> 24) {
+            case 255:
+                return PixelFormat.OPAQUE;
+            case 0:
+                return PixelFormat.TRANSPARENT;
+        }
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    @Override
+    public ConstantState getConstantState() {
+        mState.mChangingConfigurations = getChangingConfigurations();
+        return mState;
+    }
+
+    static final class ColorState extends ConstantState {
+
+        int mBaseColor; // base color, independent of setAlpha()
+        int mUseColor;  // basecolor modulated by setAlpha()
+        int mChangingConfigurations;
+
+        ColorState(ColorState state) {
+            if (state != null) {
+                mBaseColor = state.mBaseColor;
+                mUseColor = state.mUseColor;
+            }
+        }
+
+        @Override
+        public Drawable newDrawable() {
+            return new ColorDrawable(this);
+        }
+
+        @Override
+        public Drawable newDrawable(Resources res) {
+            return new ColorDrawable(this);
+        }
+
+        @Override
+        public int getChangingConfigurations() {
+            return mChangingConfigurations;
+        }
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/DraggableDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/DraggableDrawer.java
new file mode 100755
index 0000000..d0fa1ef
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/DraggableDrawer.java
@@ -0,0 +1,680 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.*;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Interpolator;
+
+public abstract class DraggableDrawer extends MenuDrawer {
+
+    /**
+     * Key used when saving menu visibility state.
+     */
+    private static final String STATE_MENU_VISIBLE = "net.simonvt.menudrawer.MenuDrawer.menuVisible";
+
+    /**
+     * Interpolator used for stretching/retracting the active indicator.
+     */
+    protected static final Interpolator INDICATOR_INTERPOLATOR = new AccelerateInterpolator();
+
+    /**
+     * Interpolator used for peeking at the drawer.
+     */
+    private static final Interpolator PEEK_INTERPOLATOR = new PeekInterpolator();
+
+    /**
+     * The maximum alpha of the dark menu overlay used for dimming the menu.
+     */
+    protected static final int MAX_MENU_OVERLAY_ALPHA = 255;
+
+    /**
+     * Default delay from {@link #peekDrawer()} is called until first animation is run.
+     */
+    private static final long DEFAULT_PEEK_START_DELAY = 5000;
+
+    /**
+     * Default delay between each subsequent animation, after {@link #peekDrawer()} has been called.
+     */
+    private static final long DEFAULT_PEEK_DELAY = 10000;
+
+    /**
+     * The duration of the peek animation.
+     */
+    protected static final int PEEK_DURATION = 5000;
+
+    /**
+     * Distance in dp from closed position from where the drawer is considered closed with regards to touch events.
+     */
+    private static final int CLOSE_ENOUGH = 3;
+
+    /**
+     * Slop before starting a drag.
+     */
+    protected int mTouchSlop;
+
+    /**
+     * Runnable used when the peek animation is running.
+     */
+    protected final Runnable mPeekRunnable = new Runnable() {
+        @Override
+        public void run () {
+            peekDrawerInvalidate();
+        }
+    };
+
+    /**
+     * Runnable used when animating the drawer open/closed.
+     */
+    private final Runnable mDragRunnable = new Runnable() {
+        @Override
+        public void run () {
+            postAnimationInvalidate();
+        }
+    };
+
+    /**
+     * Current left position of the content.
+     */
+    protected float mOffsetPixels;
+
+    /**
+     * Indicates whether the drawer is currently being dragged.
+     */
+    protected boolean mIsDragging;
+
+    /**
+     * The initial X position of a drag.
+     */
+    protected float mInitialMotionX;
+
+    /**
+     * The initial Y position of a drag.
+     */
+    protected float mInitialMotionY;
+
+    /**
+     * The last X position of a drag.
+     */
+    protected float mLastMotionX = -1;
+
+    /**
+     * The last Y position of a drag.
+     */
+    protected float mLastMotionY = -1;
+
+    /**
+     * Default delay between each subsequent animation, after {@link #peekDrawer()} has been called.
+     */
+    protected long mPeekDelay;
+
+    /**
+     * Scroller used for the peek drawer animation.
+     */
+    protected Scroller mPeekScroller;
+
+    /**
+     * Velocity tracker used when animating the drawer open/closed after a drag.
+     */
+    protected VelocityTracker mVelocityTracker;
+
+    /**
+     * Maximum velocity allowed when animating the drawer open/closed.
+     */
+    protected int mMaxVelocity;
+
+    /**
+     * Indicates whether the menu should be offset when dragging the drawer.
+     */
+    protected boolean mOffsetMenu = true;
+
+    /**
+     * Distance in px from closed position from where the drawer is considered closed with regards to touch events.
+     */
+    protected int mCloseEnough;
+
+    /**
+     * Runnable used for first call to {@link #startPeek()} after {@link #peekDrawer()}  has been called.
+     */
+    private Runnable mPeekStartRunnable;
+
+    /**
+     * Scroller used when animating the drawer open/closed.
+     */
+    private Scroller mScroller;
+
+    /**
+     * Indicates whether the current layer type is {@link android.view.View#LAYER_TYPE_HARDWARE}.
+     */
+    private boolean mLayerTypeHardware;
+
+    /**
+     * Indicates whether the menu overlay should be drawn
+     */
+    private boolean mDrawerOverlayEnabled = true;
+
+    DraggableDrawer (Activity activity, int dragMode) {
+        super(activity, dragMode);
+    }
+
+    public DraggableDrawer (Context context) {
+        super(context);
+    }
+
+    public DraggableDrawer (Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public DraggableDrawer (Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void initDrawer (Context context, AttributeSet attrs, int defStyle) {
+        super.initDrawer(context, attrs, defStyle);
+
+        final ViewConfiguration configuration = ViewConfiguration.get(context);
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mMaxVelocity = configuration.getScaledMaximumFlingVelocity();
+
+        mScroller = new Scroller(context, MenuDrawer.SMOOTH_INTERPOLATOR);
+        mPeekScroller = new Scroller(context, DraggableDrawer.PEEK_INTERPOLATOR);
+
+        mCloseEnough = dpToPx(DraggableDrawer.CLOSE_ENOUGH);
+    }
+
+    public void toggleMenu(boolean animate) {
+        if (mDrawerState == STATE_OPEN || mDrawerState == STATE_OPENING) {
+            closeMenu(animate);
+        } else if (mDrawerState == STATE_CLOSED || mDrawerState == STATE_CLOSING) {
+            openMenu(animate);
+        }
+    }
+
+    public boolean isMenuVisible() {
+        return mMenuVisible;
+    }
+
+    public void setMenuSize(final int size) {
+        mMenuSize = size;
+        mMenuSizeSet = true;
+        if (mDrawerState == STATE_OPEN || mDrawerState == STATE_OPENING) {
+            setOffsetPixels(mMenuSize);
+        }
+        requestLayout();
+        invalidate();
+    }
+
+    public void setOffsetMenuEnabled(boolean offsetMenu) {
+        if (offsetMenu != mOffsetMenu) {
+            mOffsetMenu = offsetMenu;
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    public boolean getOffsetMenuEnabled() {
+        return mOffsetMenu;
+    }
+
+    public void peekDrawer() {
+        peekDrawer(DEFAULT_PEEK_START_DELAY, DEFAULT_PEEK_DELAY);
+    }
+
+    public void peekDrawer(long delay) {
+        peekDrawer(DEFAULT_PEEK_START_DELAY, delay);
+    }
+
+    public void peekDrawer(final long startDelay, final long delay) {
+        if (startDelay < 0) {
+            throw new IllegalArgumentException("startDelay must be zero or larger.");
+        }
+        if (delay < 0) {
+            throw new IllegalArgumentException("delay must be zero or larger");
+        }
+
+        removeCallbacks(mPeekRunnable);
+        removeCallbacks(mPeekStartRunnable);
+
+        mPeekDelay = delay;
+        mPeekStartRunnable = new Runnable() {
+            @Override
+            public void run() {
+                startPeek();
+            }
+        };
+        postDelayed(mPeekStartRunnable, startDelay);
+    }
+
+    public void setHardwareLayerEnabled(boolean enabled) {
+        if (enabled != mHardwareLayersEnabled) {
+            mHardwareLayersEnabled = enabled;
+            mMenuContainer.setHardwareLayersEnabled(enabled);
+            mContentContainer.setHardwareLayersEnabled(enabled);
+            stopLayerTranslation();
+        }
+    }
+
+    public void setDrawerOverlayEnabled (boolean enabled) {
+        mDrawerOverlayEnabled = enabled;
+    }
+
+    public int getTouchMode() {
+        return mTouchMode;
+    }
+
+    public void setTouchMode(int mode) {
+        if (mTouchMode != mode) {
+            mTouchMode = mode;
+            updateTouchAreaSize();
+        }
+    }
+
+    public void setTouchBezelSize(int size) {
+        mTouchBezelSize = size;
+    }
+
+    public int getTouchBezelSize() {
+        return mTouchBezelSize;
+    }
+
+    /**
+     * Sets the number of pixels the content should be offset.
+     *
+     * @param offsetPixels The number of pixels to offset the content by.
+     */
+    protected void setOffsetPixels(float offsetPixels) {
+        final int oldOffset = (int) mOffsetPixels;
+        final int newOffset = (int) offsetPixels;
+
+        mOffsetPixels = offsetPixels;
+
+        if (newOffset != oldOffset) {
+            onOffsetPixelsChanged(newOffset);
+            mMenuVisible = newOffset != 0;
+        }
+    }
+
+    /**
+     * Called when the number of pixels the content should be offset by has changed.
+     *
+     * @param offsetPixels The number of pixels to offset the content by.
+     */
+    protected abstract void onOffsetPixelsChanged(int offsetPixels);
+
+    /**
+     * If possible, set the layer type to {@link android.view.View#LAYER_TYPE_HARDWARE}.
+     */
+    protected void startLayerTranslation() {
+        if (USE_TRANSLATIONS && mHardwareLayersEnabled && !mLayerTypeHardware) {
+            mLayerTypeHardware = true;
+            mContentContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            mMenuContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        }
+    }
+
+    /**
+     * If the current layer type is {@link android.view.View#LAYER_TYPE_HARDWARE}, this will set it to
+     * {@link View#LAYER_TYPE_NONE}.
+     */
+    private void stopLayerTranslation() {
+        if (mLayerTypeHardware) {
+            mLayerTypeHardware = false;
+            mContentContainer.setLayerType(View.LAYER_TYPE_NONE, null);
+            mMenuContainer.setLayerType(View.LAYER_TYPE_NONE, null);
+        }
+    }
+
+    /**
+     * Compute the touch area based on the touch mode.
+     */
+    protected void updateTouchAreaSize() {
+        if (mTouchMode == TOUCH_MODE_BEZEL) {
+            mTouchSize = mTouchBezelSize;
+        } else if (mTouchMode == TOUCH_MODE_FULLSCREEN) {
+            mTouchSize = getMeasuredWidth();
+        } else {
+            mTouchSize = 0;
+        }
+    }
+
+    /**
+     * Called when a drag has been ended.
+     */
+    protected void endDrag() {
+        mIsDragging = false;
+
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    /**
+     * Stops ongoing animation of the drawer.
+     */
+    protected void stopAnimation() {
+        removeCallbacks(mDragRunnable);
+        mScroller.abortAnimation();
+        stopLayerTranslation();
+    }
+
+    /**
+     * Called when a drawer animation has successfully completed.
+     */
+    private void completeAnimation() {
+        mScroller.abortAnimation();
+        final int finalX = mScroller.getFinalX();
+        setOffsetPixels(finalX);
+        setDrawerState(finalX == 0 ? STATE_CLOSED : STATE_OPEN);
+        stopLayerTranslation();
+    }
+
+    /**
+     * Moves the drawer to the position passed.
+     *
+     * @param position The position the content is moved to.
+     * @param velocity Optional velocity if called by releasing a drag event.
+     * @param animate  Whether the move is animated.
+     */
+    protected void animateOffsetTo(int position, int velocity, boolean animate) {
+        endDrag();
+        endPeek();
+
+        final int startX = (int) mOffsetPixels;
+        final int dx = position - startX;
+        if (dx == 0 || !animate) {
+            setOffsetPixels(position);
+            setDrawerState(position == 0 ? STATE_CLOSED : STATE_OPEN);
+            stopLayerTranslation();
+            return;
+        }
+
+        int duration;
+
+        velocity = Math.abs(velocity);
+        if (velocity > 0) {
+            duration = 4 * Math.round(1000.f * Math.abs((float) dx / velocity));
+        } else {
+            duration = (int) (600.f * Math.abs((float) dx / mMenuSize));
+        }
+
+        duration = Math.min(duration, mMaxAnimationDuration);
+
+        if (dx > 0) {
+            setDrawerState(STATE_OPENING);
+            mScroller.startScroll(startX, 0, dx, 0, duration);
+        } else {
+            setDrawerState(STATE_CLOSING);
+            mScroller.startScroll(startX, 0, dx, 0, duration);
+        }
+
+        startLayerTranslation();
+
+        postAnimationInvalidate();
+    }
+
+    /**
+     * Callback when each frame in the drawer animation should be drawn.
+     */
+    private void postAnimationInvalidate() {
+        if (mScroller.computeScrollOffset()) {
+            final int oldX = (int) mOffsetPixels;
+            final int x = mScroller.getCurrX();
+
+            if (x != oldX) setOffsetPixels(x);
+            if (x != mScroller.getFinalX()) {
+                postOnAnimation(mDragRunnable);
+                return;
+            }
+        }
+
+        completeAnimation();
+    }
+
+    /**
+     * Starts peek drawer animation.
+     */
+    protected void startPeek() {
+        initPeekScroller();
+
+        startLayerTranslation();
+        peekDrawerInvalidate();
+    }
+
+    protected abstract void initPeekScroller();
+
+    /**
+     * Callback when each frame in the peek drawer animation should be drawn.
+     */
+    private void peekDrawerInvalidate() {
+        if (mPeekScroller.computeScrollOffset()) {
+            final int oldX = (int) mOffsetPixels;
+            final int x = mPeekScroller.getCurrX();
+            if (x != oldX) setOffsetPixels(x);
+
+            if (!mPeekScroller.isFinished()) {
+                postOnAnimation(mPeekRunnable);
+                return;
+
+            } else if (mPeekDelay > 0) {
+                mPeekStartRunnable = new Runnable() {
+                    @Override
+                    public void run() {
+                        startPeek();
+                    }
+                };
+                postDelayed(mPeekStartRunnable, mPeekDelay);
+            }
+        }
+
+        completePeek();
+    }
+
+    /**
+     * Called when the peek drawer animation has successfully completed.
+     */
+    private void completePeek() {
+        mPeekScroller.abortAnimation();
+
+        setOffsetPixels(0);
+
+        setDrawerState(STATE_CLOSED);
+        stopLayerTranslation();
+    }
+
+    /**
+     * Stops ongoing peek drawer animation.
+     */
+    protected void endPeek() {
+        removeCallbacks(mPeekStartRunnable);
+        removeCallbacks(mPeekRunnable);
+        stopLayerTranslation();
+    }
+
+    protected boolean isCloseEnough() {
+        return Math.abs(mOffsetPixels) <= mCloseEnough;
+    }
+
+    /**
+     * Returns true if the touch event occurs over the content.
+     *
+     * @param ev The motion event.
+     * @return True if the touch event occurred over the content, false otherwise.
+     */
+    protected abstract boolean isContentTouch(MotionEvent ev);
+
+    /**
+     * Returns true if dragging the content should be allowed.
+     *
+     * @param ev The motion event.
+     * @return True if dragging the content should be allowed, false otherwise.
+     */
+    protected abstract boolean onDownAllowDrag(MotionEvent ev);
+
+    /**
+     * Tests scrollability within child views of v given a delta of dx.
+     *
+     * @param v      View to test for horizontal scrollability
+     * @param checkV Whether the view should be checked for draggability
+     * @param dx     Delta scrolled in pixels
+     * @param x      X coordinate of the active touch point
+     * @param y      Y coordinate of the active touch point
+     * @return true if child views of v can be scrolled by delta of dx.
+     */
+    protected boolean canChildScrollHorizontally(View v, boolean checkV, int dx, int x, int y) {
+        if (v instanceof ViewGroup) {
+            final ViewGroup group = (ViewGroup) v;
+
+            final int count = group.getChildCount();
+            // Count backwards - let topmost views consume scroll distance first.
+            for (int i = count - 1; i >= 0; i--) {
+                final View child = group.getChildAt(i);
+
+                final int childLeft = child.getLeft() + supportGetTranslationX(child);
+                final int childRight = child.getRight() + supportGetTranslationX(child);
+                final int childTop = child.getTop() + supportGetTranslationY(child);
+                final int childBottom = child.getBottom() + supportGetTranslationY(child);
+
+                if (x >= childLeft && x < childRight && y >= childTop && y < childBottom
+                        &&  canChildScrollHorizontally(child, true, dx, x - childLeft, y - childTop)) {
+                    return true;
+                }
+            }
+        }
+
+        return checkV && mOnInterceptMoveEventListener.isViewDraggable(v, dx, x, y);
+    }
+
+    /**
+     * Tests scrollability within child views of v given a delta of dx.
+     *
+     * @param v      View to test for horizontal scrollability
+     * @param checkV Whether the view should be checked for draggability
+     * @param dx     Delta scrolled in pixels
+     * @param x      X coordinate of the active touch point
+     * @param y      Y coordinate of the active touch point
+     * @return true if child views of v can be scrolled by delta of dx.
+     */
+    protected boolean canChildScrollVertically(View v, boolean checkV, int dx, int x, int y) {
+        if (v instanceof ViewGroup) {
+            final ViewGroup group = (ViewGroup) v;
+
+            final int count = group.getChildCount();
+            // Count backwards - let topmost views consume scroll distance first.
+            for (int i = count - 1; i >= 0; i--) {
+                final View child = group.getChildAt(i);
+
+                final int childLeft = child.getLeft() + supportGetTranslationX(child);
+                final int childRight = child.getRight() + supportGetTranslationX(child);
+                final int childTop = child.getTop() + supportGetTranslationY(child);
+                final int childBottom = child.getBottom() + supportGetTranslationY(child);
+
+                if (x >= childLeft && x < childRight && y >= childTop && y < childBottom
+                        && canChildScrollVertically(child, true, dx, x - childLeft, y - childTop)) {
+                    return true;
+                }
+            }
+        }
+
+        return checkV && mOnInterceptMoveEventListener.isViewDraggable(v, dx, x, y);
+    }
+
+    private int supportGetTranslationY(View v) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            return (int) v.getTranslationY();
+        }
+
+        return 0;
+    }
+
+    private int supportGetTranslationX(View v) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            return (int) v.getTranslationX();
+        }
+
+        return 0;
+    }
+
+    /**
+     * Returns true if dragging the content should be allowed.
+     *
+     * @param ev The motion event.
+     * @return True if dragging the content should be allowed, false otherwise.
+     */
+    protected abstract boolean onMoveAllowDrag(MotionEvent ev, float dx);
+
+    /**
+     * Called when a move event has happened while dragging the content is in progress.
+     *
+     * @param dx The X difference between the last motion event and the current motion event.
+     */
+    protected abstract void onMoveEvent(float dx);
+
+    /**
+     * Called when {@link android.view.MotionEvent#ACTION_UP} of {@link android.view.MotionEvent#ACTION_CANCEL} is
+     * delivered to {@link net.simonvt.menudrawer.MenuDrawer#onTouchEvent(android.view.MotionEvent)}.
+     *
+     * @param ev The motion event.
+     */
+    protected abstract void onUpEvent(MotionEvent ev, boolean isDownOnOutside);
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+        final int offsetPixels = (int) mOffsetPixels;
+
+        if (offsetPixels != 0 && mDrawerOverlayEnabled) drawMenuOverlay(canvas, offsetPixels);
+        if (offsetPixels != 0 && mWindowEdge != null) drawWindowEdge(canvas, offsetPixels);
+        if (mDropShadowEnabled) drawDropShadow(canvas, offsetPixels);
+        if (mActiveIndicator != null) drawIndicator(canvas, offsetPixels);
+    }
+
+    /**
+     * Called when the content drop shadow should be drawn.
+     *
+     * @param canvas       The canvas on which to draw.
+     * @param offsetPixels Value in pixels indicating the offset.
+     */
+    protected abstract void drawDropShadow(Canvas canvas, int offsetPixels);
+
+    /**
+     * Called when the menu overlay should be drawn.
+     *
+     * @param canvas       The canvas on which to draw.
+     * @param offsetPixels Value in pixels indicating the offset.
+     */
+    protected abstract void drawMenuOverlay(Canvas canvas, int offsetPixels);
+
+    /**
+     * Called when the active indicator should be drawn.
+     *
+     * @param canvas       The canvas on which to draw.
+     * @param offsetPixels Value in pixels indicating the offset.
+     */
+    protected abstract void drawIndicator(Canvas canvas, int offsetPixels);
+
+    protected abstract void drawWindowEdge(Canvas canvas, int offsetPixels);
+
+    void saveState(Bundle state) {
+        final boolean menuVisible = mDrawerState == STATE_OPEN || mDrawerState == STATE_OPENING;
+        state.putBoolean(STATE_MENU_VISIBLE, menuVisible);
+    }
+
+    public void restoreState(Parcelable in) {
+        super.restoreState(in);
+        Bundle state = (Bundle) in;
+        final boolean menuOpen = state.getBoolean(STATE_MENU_VISIBLE);
+        if (menuOpen) {
+            openMenu(false);
+        } else {
+            setOffsetPixels(0);
+        }
+        mDrawerState = menuOpen ? STATE_OPEN : STATE_CLOSED;
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/FloatScroller.java b/MenuDrawer/src/net/simonvt/menudrawer/FloatScroller.java
new file mode 100755
index 0000000..df5b445
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/FloatScroller.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * 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 net.simonvt.menudrawer;
+
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+/**
+ * This class encapsulates scrolling.  The duration of the scroll
+ * can be passed in the constructor and specifies the maximum time that
+ * the scrolling animation should take.  Past this time, the scrolling is
+ * automatically moved to its final stage and computeScrollOffset()
+ * will always return false to indicate that scrolling is over.
+ */
+public class FloatScroller {
+
+    private float mStart;
+    private float mFinal;
+
+    private float mCurr;
+    private long mStartTime;
+    private int mDuration;
+    private float mDurationReciprocal;
+    private float mDeltaX;
+    private boolean mFinished;
+    private Interpolator mInterpolator;
+
+    /**
+     * Create a Scroller with the specified interpolator. If the interpolator is
+     * null, the default (viscous) interpolator will be used. Specify whether or
+     * not to support progressive "flywheel" behavior in flinging.
+     */
+    public FloatScroller(Interpolator interpolator) {
+        mFinished = true;
+        mInterpolator = interpolator;
+    }
+
+    /**
+     * Returns whether the scroller has finished scrolling.
+     *
+     * @return True if the scroller has finished scrolling, false otherwise.
+     */
+    public final boolean isFinished() {
+        return mFinished;
+    }
+
+    /**
+     * Force the finished field to a particular value.
+     *
+     * @param finished The new finished value.
+     */
+    public final void forceFinished(boolean finished) {
+        mFinished = finished;
+    }
+
+    /**
+     * Returns how long the scroll event will take, in milliseconds.
+     *
+     * @return The duration of the scroll in milliseconds.
+     */
+    public final int getDuration() {
+        return mDuration;
+    }
+
+    /**
+     * Returns the current offset in the scroll.
+     *
+     * @return The new offset as an absolute distance from the origin.
+     */
+    public final float getCurr() {
+        return mCurr;
+    }
+
+    /**
+     * Returns the start offset in the scroll.
+     *
+     * @return The start offset as an absolute distance from the origin.
+     */
+    public final float getStart() {
+        return mStart;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final offset as an absolute distance from the origin.
+     */
+    public final float getFinal() {
+        return mFinal;
+    }
+
+    public boolean computeScrollOffset() {
+        if (mFinished) {
+            return false;
+        }
+
+        int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+
+        if (timePassed < mDuration) {
+            float x = timePassed * mDurationReciprocal;
+            x = mInterpolator.getInterpolation(x);
+            mCurr = mStart + x * mDeltaX;
+
+        } else {
+            mCurr = mFinal;
+            mFinished = true;
+        }
+        return true;
+    }
+
+    public void startScroll(float start, float delta, int duration) {
+        mFinished = false;
+        mDuration = duration;
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mStart = start;
+        mFinal = start + delta;
+        mDeltaX = delta;
+        mDurationReciprocal = 1.0f / (float) mDuration;
+    }
+
+    /**
+     * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+     * aborting the animating cause the scroller to move to the final x and y
+     * position
+     *
+     * @see #forceFinished(boolean)
+     */
+    public void abortAnimation() {
+        mCurr = mFinal;
+        mFinished = true;
+    }
+
+    /**
+     * Extend the scroll animation. This allows a running animation to scroll
+     * further and longer, when used with {@link #setFinal(float)}.
+     *
+     * @param extend Additional time to scroll in milliseconds.
+     * @see #setFinal(float)
+     */
+    public void extendDuration(int extend) {
+        int passed = timePassed();
+        mDuration = passed + extend;
+        mDurationReciprocal = 1.0f / mDuration;
+        mFinished = false;
+    }
+
+    /**
+     * Returns the time elapsed since the beginning of the scrolling.
+     *
+     * @return The elapsed time in milliseconds.
+     */
+    public int timePassed() {
+        return (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+    }
+
+    public void setFinal(float newVal) {
+        mFinal = newVal;
+        mDeltaX = mFinal - mStart;
+        mFinished = false;
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/HorizontalDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/HorizontalDrawer.java
new file mode 100755
index 0000000..0f25684
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/HorizontalDrawer.java
@@ -0,0 +1,211 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+
+public abstract class HorizontalDrawer extends DraggableDrawer {
+
+    private static final String TAG = "HorizontalDrawer";
+
+    HorizontalDrawer(Activity activity, int dragMode) {
+        super(activity, dragMode);
+    }
+
+    public HorizontalDrawer(Context context) {
+        super(context);
+    }
+
+    public HorizontalDrawer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public HorizontalDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+        if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
+            throw new IllegalStateException("Must measure with an exact size");
+        }
+
+        final int width = MeasureSpec.getSize(widthMeasureSpec);
+        final int height = MeasureSpec.getSize(heightMeasureSpec);
+
+        if (!mMenuSizeSet) mMenuSize = (int) (width * 0.8f);
+        if (mOffsetPixels == -1) openMenu(false);
+
+        final int menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, mMenuSize);
+        final int menuHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height);
+        mMenuContainer.measure(menuWidthMeasureSpec, menuHeightMeasureSpec);
+
+        final int contentWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);
+        final int contentHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height);
+        mContentContainer.measure(contentWidthMeasureSpec, contentHeightMeasureSpec);
+
+        setMeasuredDimension(width, height);
+
+        updateTouchAreaSize();
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+        if (action == MotionEvent.ACTION_DOWN && mMenuVisible && isCloseEnough()) {
+            setOffsetPixels(0);
+            stopAnimation();
+            endPeek();
+            setDrawerState(STATE_CLOSED);
+        }
+
+        // Always intercept events over the content while menu is visible.
+        if (mMenuVisible && isContentTouch(ev)) return true;
+
+        if (mTouchMode == TOUCH_MODE_NONE) {
+            return false;
+        }
+
+        if (action != MotionEvent.ACTION_DOWN) {
+            if (mIsDragging) return true;
+        }
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                mLastMotionX = mInitialMotionX = ev.getX();
+                mLastMotionY = mInitialMotionY = ev.getY();
+                final boolean allowDrag = onDownAllowDrag(ev);
+
+                if (allowDrag) {
+                    setDrawerState(mMenuVisible ? STATE_OPEN : STATE_CLOSED);
+                    stopAnimation();
+                    endPeek();
+                    mIsDragging = false;
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_MOVE: {
+                final float x = ev.getX();
+                final float dx = x - mLastMotionX;
+                final float xDiff = Math.abs(dx);
+                final float y = ev.getY();
+                final float yDiff = Math.abs(y - mLastMotionY);
+
+                if (xDiff > mTouchSlop && xDiff > yDiff) {
+                    if (mOnInterceptMoveEventListener != null && mTouchMode == TOUCH_MODE_FULLSCREEN
+                            && canChildScrollHorizontally(mContentContainer, false, (int) dx, (int) x, (int) y)) {
+                        endDrag(); // Release the velocity tracker
+                        return false;
+                    }
+
+                    final boolean allowDrag = onMoveAllowDrag(ev, dx);
+
+                    if (allowDrag) {
+                        setDrawerState(STATE_DRAGGING);
+                        mIsDragging = true;
+                        mLastMotionX = x;
+                        mLastMotionY = y;
+                    }
+                }
+                break;
+            }
+
+            /**
+             * If you click really fast, an up or cancel event is delivered here.
+             * Just snap content to whatever is closest.
+             * */
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP: {
+                if (Math.abs(mOffsetPixels) > mMenuSize / 2) {
+                    openMenu();
+                } else {
+                    closeMenu();
+                }
+                break;
+            }
+        }
+
+        if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain();
+        mVelocityTracker.addMovement(ev);
+
+        return mIsDragging;
+    }
+
+    private boolean mIsDownOnOutside = false;
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (!mMenuVisible && !mIsDragging && (mTouchMode == TOUCH_MODE_NONE)) {
+            return false;
+        }
+        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+        if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain();
+        mVelocityTracker.addMovement(ev);
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                mIsDownOnOutside = ev.getX() <= getWidth() + mOffsetPixels;
+                mLastMotionX = mInitialMotionX = ev.getX();
+                mLastMotionY = mInitialMotionY = ev.getY();
+                final boolean allowDrag = onDownAllowDrag(ev);
+
+                if (allowDrag) {
+                    stopAnimation();
+                    endPeek();
+                    startLayerTranslation();
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_MOVE: {
+                if (!mIsDragging) {
+                    final float x = ev.getX();
+                    final float dx = x - mLastMotionX;
+                    final float xDiff = Math.abs(dx);
+                    final float y = ev.getY();
+                    final float yDiff = Math.abs(y - mLastMotionY);
+
+                    if (xDiff > mTouchSlop && xDiff > yDiff) {
+                        final boolean allowDrag = onMoveAllowDrag(ev, dx);
+
+                        if (allowDrag) {
+                            setDrawerState(STATE_DRAGGING);
+                            mIsDragging = true;
+                            mLastMotionX = x - mInitialMotionX > 0
+                                    ? mInitialMotionX + mTouchSlop
+                                    : mInitialMotionX - mTouchSlop;
+                        }
+                    }
+                }
+
+                if (mIsDragging) {
+                    startLayerTranslation();
+
+                    final float x = ev.getX();
+                    final float dx = x - mLastMotionX;
+
+                    mLastMotionX = x;
+                    onMoveEvent(dx);
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP: {
+                onUpEvent(ev, mIsDownOnOutside);
+                mIsDownOnOutside = false;
+                break;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/LeftDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/LeftDrawer.java
new file mode 100755
index 0000000..c204ae7
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/LeftDrawer.java
@@ -0,0 +1,225 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+public class LeftDrawer extends HorizontalDrawer {
+
+    private int mIndicatorTop;
+
+    LeftDrawer(Activity activity, int dragMode) {
+        super(activity, dragMode);
+    }
+
+    public LeftDrawer(Context context) {
+        super(context);
+    }
+
+    public LeftDrawer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public LeftDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void openMenu(boolean animate) {
+        animateOffsetTo(mMenuSize, 0, animate);
+    }
+
+    @Override
+    public void closeMenu(boolean animate) {
+        animateOffsetTo(0, 0, animate);
+    }
+
+    @Override
+    public void setDropShadowColor(int color) {
+        final int endColor = color & 0x00FFFFFF;
+        mDropShadowDrawable = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, new int[] {
+                color,
+                endColor,
+        });
+        invalidate();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int width = r - l;
+        final int height = b - t;
+        final int offsetPixels = (int) mOffsetPixels;
+
+        mMenuContainer.layout(0, 0, mMenuSize, height);
+        offsetMenu(offsetPixels);
+
+        if (USE_TRANSLATIONS) {
+            mContentContainer.layout(0, 0, width, height);
+        } else {
+            mContentContainer.layout(offsetPixels, 0, width + offsetPixels, height);
+        }
+    }
+
+    /**
+     * Offsets the menu relative to its original position based on the position of the content.
+     *
+     * @param offsetPixels The number of pixels the content if offset.
+     */
+    private void offsetMenu(int offsetPixels) {
+        if (mOffsetMenu && mMenuSize != 0) {
+            final int menuWidth = mMenuSize;
+            final float openRatio = (menuWidth - (float) offsetPixels) / menuWidth;
+
+            if (USE_TRANSLATIONS) {
+                if (offsetPixels > 0) {
+                    final int menuLeft = (int) (0.25f * (-openRatio * menuWidth));
+                    mMenuContainer.setTranslationX(menuLeft);
+                } else {
+                    mMenuContainer.setTranslationX(-menuWidth);
+                }
+
+            } else {
+                final int oldMenuLeft = mMenuContainer.getLeft();
+                final int offset = (int) (0.25f * (-openRatio * menuWidth)) - oldMenuLeft;
+                mMenuContainer.offsetLeftAndRight(offset);
+                mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
+            }
+        }
+    }
+
+    @Override
+    protected void drawDropShadow(Canvas canvas, int offsetPixels) {
+        final int height = getHeight();
+
+        mDropShadowDrawable.setBounds(offsetPixels - mDropShadowSize, 0, offsetPixels, height);
+        mDropShadowDrawable.draw(canvas);
+    }
+
+    @Override
+    protected void drawMenuOverlay(Canvas canvas, int offsetPixels) {
+        final int height = getHeight();
+        final float openRatio = ((float) offsetPixels) / mMenuSize;
+
+        mMenuOverlay.setBounds(0, 0, offsetPixels, height);
+        mMenuOverlay.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * (1.f - openRatio)));
+        mMenuOverlay.draw(canvas);
+    }
+
+    @Override
+    protected void drawWindowEdge (Canvas canvas, int offsetPixels) {
+        final int height = getHeight();
+        final int edgeWidth = mWindowEdge.getIntrinsicWidth();
+        final int left = offsetPixels;
+        final int right = offsetPixels + edgeWidth;
+        final float openRatio = ((float) Math.abs(offsetPixels)) / mMenuSize;
+
+        mWindowEdge.setBounds(left, 0, right, height);
+        mWindowEdge.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * (openRatio)));
+        mWindowEdge.draw(canvas);
+    }
+
+    @Override
+    protected void drawIndicator(Canvas canvas, int offsetPixels) {
+        if (mActiveView != null && isViewDescendant(mActiveView)) {
+            Integer position = (Integer) mActiveView.getTag(R.id.mdActiveViewPosition);
+            final int pos = position == null ? 0 : position;
+
+            if (pos == mActivePosition) {
+                final float openRatio = ((float) offsetPixels) / mMenuSize;
+
+                mActiveView.getDrawingRect(mActiveRect);
+                offsetDescendantRectToMyCoords(mActiveView, mActiveRect);
+
+                final float interpolatedRatio = 1.f - INDICATOR_INTERPOLATOR.getInterpolation((1.f - openRatio));
+                final int interpolatedWidth = (int) (mActiveIndicator.getWidth() * interpolatedRatio);
+
+                if (mIndicatorAnimating) {
+                    final int indicatorFinalTop = mActiveRect.top + ((mActiveRect.height()
+                            - mActiveIndicator.getHeight()) / 2);
+                    final int indicatorStartTop = mIndicatorStartPos;
+                    final int diff = indicatorFinalTop - indicatorStartTop;
+                    final int startOffset = (int) (diff * mIndicatorOffset);
+                    mIndicatorTop = indicatorStartTop + startOffset;
+                } else {
+                    mIndicatorTop = mActiveRect.top + ((mActiveRect.height() - mActiveIndicator.getHeight()) / 2);
+                }
+                final int right = offsetPixels;
+                final int left = right - interpolatedWidth;
+
+                canvas.save();
+                canvas.clipRect(left, 0, right, getHeight());
+                canvas.drawBitmap(mActiveIndicator, left, mIndicatorTop, null);
+                canvas.restore();
+            }
+        }
+    }
+
+    @Override
+    protected int getIndicatorStartPos() {
+        return mIndicatorTop;
+    }
+
+    @Override
+    protected void initPeekScroller() {
+        final int dx = mMenuSize / 3;
+        mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
+    }
+
+    @Override
+    protected void onOffsetPixelsChanged(int offsetPixels) {
+        if (USE_TRANSLATIONS) {
+            mContentContainer.setTranslationX(offsetPixels);
+            offsetMenu(offsetPixels);
+            invalidate();
+        } else {
+            mContentContainer.offsetLeftAndRight(offsetPixels - mContentContainer.getLeft());
+            offsetMenu(offsetPixels);
+            invalidate();
+        }
+    }
+
+    //////////////////////////////////////////////////////////////////////
+    // Touch handling
+    //////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected boolean isContentTouch(MotionEvent ev) {
+        return ev.getX() > mOffsetPixels;
+    }
+
+    @Override
+    protected boolean onDownAllowDrag(MotionEvent ev) {
+        return (!mMenuVisible && mInitialMotionX <= mTouchSize)
+                || (mMenuVisible && mInitialMotionX >= mOffsetPixels);
+    }
+
+    @Override
+    protected boolean onMoveAllowDrag(MotionEvent ev, float diff) {
+        return (!mMenuVisible && mInitialMotionX <= mTouchSize && (diff > 0))
+                || (mMenuVisible && mInitialMotionX >= mOffsetPixels);
+    }
+
+    @Override
+    protected void onMoveEvent(float dx) {
+        setOffsetPixels(Math.min(Math.max(mOffsetPixels + dx, 0), mMenuSize));
+    }
+
+    @Override
+    protected void onUpEvent(MotionEvent ev, boolean isDownOnOutside) {
+        final int offsetPixels = (int) mOffsetPixels;
+
+        if (mIsDragging) {
+            mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+            final int initialVelocity = (int) mVelocityTracker.getXVelocity();
+            mLastMotionX = ev.getX();
+            animateOffsetTo(mVelocityTracker.getXVelocity() > 0 ? mMenuSize : 0, initialVelocity, true);
+
+            // Close the menu when content is clicked while the menu is visible.
+        } else if (mMenuVisible && ev.getX() > offsetPixels) {
+            closeMenu();
+        }
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/LeftStaticDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/LeftStaticDrawer.java
new file mode 100755
index 0000000..47fa75a
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/LeftStaticDrawer.java
@@ -0,0 +1,80 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+
+public class LeftStaticDrawer extends StaticDrawer {
+
+    private int mIndicatorTop;
+
+    LeftStaticDrawer(Activity activity, int dragMode) {
+        super(activity, dragMode);
+    }
+
+    public LeftStaticDrawer(Context context) {
+        super(context);
+    }
+
+    public LeftStaticDrawer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public LeftStaticDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void initDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super.initDrawer(context, attrs, defStyle);
+        mPosition = Position.LEFT;
+    }
+
+    @Override
+    public void setDropShadowColor(int color) {
+        final int endColor = color & 0x00FFFFFF;
+        mDropShadowDrawable = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, new int[] {
+                color,
+                endColor,
+        });
+        invalidate();
+    }
+
+    @Override
+    protected void drawIndicator(Canvas canvas) {
+        if (mActiveView != null && isViewDescendant(mActiveView)) {
+            Integer position = (Integer) mActiveView.getTag(R.id.mdActiveViewPosition);
+            final int pos = position == null ? 0 : position;
+
+            if (pos == mActivePosition) {
+                mActiveView.getDrawingRect(mActiveRect);
+                offsetDescendantRectToMyCoords(mActiveView, mActiveRect);
+
+                if (mIndicatorAnimating) {
+                    final int indicatorFinalTop = mActiveRect.top + ((mActiveRect.height()
+                            - mActiveIndicator.getHeight()) / 2);
+                    final int indicatorStartTop = mIndicatorStartPos;
+                    final int diff = indicatorFinalTop - indicatorStartTop;
+                    final int startOffset = (int) (diff * mIndicatorOffset);
+                    mIndicatorTop = indicatorStartTop + startOffset;
+                } else {
+                    mIndicatorTop = mActiveRect.top + ((mActiveRect.height() - mActiveIndicator.getHeight()) / 2);
+                }
+                final int right = mMenuSize;
+                final int left = right - mActiveIndicator.getWidth();
+
+                canvas.save();
+                canvas.clipRect(left, 0, right, getHeight());
+                canvas.drawBitmap(mActiveIndicator, left, mIndicatorTop, null);
+                canvas.restore();
+            }
+        }
+    }
+
+    @Override
+    protected int getIndicatorStartPos() {
+        return mIndicatorTop;
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/MenuDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/MenuDrawer.java
new file mode 100755
index 0000000..21f29d6
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/MenuDrawer.java
@@ -0,0 +1,1157 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.*;
+import android.view.animation.Interpolator;
+
+public abstract class MenuDrawer extends ViewGroup {
+
+    /**
+     * Callback interface for changing state of the drawer.
+     */
+    public interface OnDrawerStateChangeListener {
+
+        /**
+         * Called when the drawer state changes.
+         *
+         * @param oldState The old drawer state.
+         * @param newState The new drawer state.
+         */
+        void onDrawerStateChange(int oldState, int newState);
+    }
+
+    /**
+     * Callback that is invoked when the drawer is in the process of deciding whether it should intercept the touch
+     * event. This lets the listener decide if the pointer is on a view that would disallow dragging of the drawer.
+     * This is only called when the touch mode is {@link #TOUCH_MODE_FULLSCREEN}.
+     */
+    public interface OnInterceptMoveEventListener {
+
+        /**
+         * Called for each child the pointer i on when the drawer is deciding whether to intercept the touch event.
+         *
+         * @param v  View to test for draggability
+         * @param dx Delta drag in pixels
+         * @param x  X coordinate of the active touch point
+         * @param y  Y coordinate of the active touch point
+         * @return true if view is draggable by delta dx.
+         */
+        boolean isViewDraggable(View v, int dx, int x, int y);
+    }
+
+    /**
+     * Tag used when logging.
+     */
+    private static final String TAG = "MenuDrawer";
+
+    /**
+     * Indicates whether debug code should be enabled.
+     */
+    private static final boolean DEBUG = false;
+
+    /**
+     * The time between each frame when animating the drawer.
+     */
+    protected static final int ANIMATION_DELAY = 1000 / 60;
+
+    /**
+     * The default touch bezel size of the drawer in dp.
+     */
+    private static final int DEFAULT_DRAG_BEZEL_DP = 24;
+
+    /**
+     * The default drop shadow size in dp.
+     */
+    private static final int DEFAULT_DROP_SHADOW_DP = 6;
+
+    /**
+     * Drag mode for sliding only the content view.
+     */
+    public static final int MENU_DRAG_CONTENT = 0;
+
+    /**
+     * Drag mode for sliding the entire window.
+     */
+    public static final int MENU_DRAG_WINDOW = 1;
+
+    /**
+     * Disallow opening the drawer by dragging the screen.
+     */
+    public static final int TOUCH_MODE_NONE = 0;
+
+    /**
+     * Allow opening drawer only by dragging on the edge of the screen.
+     */
+    public static final int TOUCH_MODE_BEZEL = 1;
+
+    /**
+     * Allow opening drawer by dragging anywhere on the screen.
+     */
+    public static final int TOUCH_MODE_FULLSCREEN = 2;
+
+    /**
+     * Indicates that the drawer is currently closed.
+     */
+    public static final int STATE_CLOSED = 0;
+
+    /**
+     * Indicates that the drawer is currently closing.
+     */
+    public static final int STATE_CLOSING = 1;
+
+    /**
+     * Indicates that the drawer is currently being dragged by the user.
+     */
+    public static final int STATE_DRAGGING = 2;
+
+    /**
+     * Indicates that the drawer is currently opening.
+     */
+    public static final int STATE_OPENING = 4;
+
+    /**
+     * Indicates that the drawer is currently open.
+     */
+    public static final int STATE_OPEN = 8;
+
+    /**
+     * Indicates whether to use {@link View#setTranslationX(float)} when positioning views.
+     */
+    static final boolean USE_TRANSLATIONS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1;
+
+    /**
+     * Time to animate the indicator to the new active view.
+     */
+    static final int INDICATOR_ANIM_DURATION = 800;
+
+    /**
+     * The maximum animation duration.
+     */
+    private static final int DEFAULT_ANIMATION_DURATION = 600;
+
+    /**
+     * Interpolator used when animating the drawer open/closed.
+     */
+    protected static final Interpolator SMOOTH_INTERPOLATOR = new SmoothInterpolator();
+
+    /**
+     * Drawable used as menu overlay.
+     */
+    protected Drawable mMenuOverlay;
+
+    /**
+     * Defines whether the drop shadow is enabled.
+     */
+    protected boolean mDropShadowEnabled;
+
+    /**
+     * Drawable used as content drop shadow onto the menu.
+     */
+    protected Drawable mDropShadowDrawable;
+
+    /**
+     * The size of the content drop shadow.
+     */
+    protected int mDropShadowSize;
+
+    /**
+     * Bitmap used to indicate the active view.
+     */
+    protected Bitmap mActiveIndicator;
+
+    /**
+     * The currently active view.
+     */
+    protected View mActiveView;
+
+    /**
+     * Position of the active view. This is compared to View#getTag(R.id.mdActiveViewPosition) when drawing the
+     * indicator.
+     */
+    protected int mActivePosition;
+
+    /**
+     * Whether the indicator should be animated between positions.
+     */
+    private boolean mAllowIndicatorAnimation;
+
+    /**
+     * Used when reading the position of the active view.
+     */
+    protected final Rect mActiveRect = new Rect();
+
+    /**
+     * Temporary {@link Rect} used for deciding whether the view should be invalidated so the indicator can be redrawn.
+     */
+    private final Rect mTempRect = new Rect();
+
+    /**
+     * The custom menu view set by the user.
+     */
+    private View mMenuView;
+
+    /**
+     * The parent of the menu view.
+     */
+    protected BuildLayerFrameLayout mMenuContainer;
+
+    /**
+     * The parent of the content view.
+     */
+    protected BuildLayerFrameLayout mContentContainer;
+
+    /**
+     * The size of the menu (width or height depending on the gravity).
+     */
+    protected int mMenuSize;
+
+    /**
+     * Indicates whether the menu size has been set explicity either via the theme or by calling
+     * {@link #setMenuSize(int)}.
+     */
+    protected boolean mMenuSizeSet;
+
+    /**
+     * Indicates whether the menu is currently visible.
+     */
+    protected boolean mMenuVisible;
+
+    /**
+     * The drag mode of the drawer. Can be either {@link #MENU_DRAG_CONTENT} or {@link #MENU_DRAG_WINDOW}.
+     */
+    private int mDragMode = MENU_DRAG_CONTENT;
+
+    /**
+     * The current drawer state.
+     *
+     * @see #STATE_CLOSED
+     * @see #STATE_CLOSING
+     * @see #STATE_DRAGGING
+     * @see #STATE_OPENING
+     * @see #STATE_OPEN
+     */
+    protected int mDrawerState = STATE_CLOSED;
+
+    /**
+     * The touch bezel size of the drawer in px.
+     */
+    protected int mTouchBezelSize;
+
+    /**
+     * The touch area size of the drawer in px.
+     */
+    protected int mTouchSize;
+
+    /**
+     * Listener used to dispatch state change events.
+     */
+    private OnDrawerStateChangeListener mOnDrawerStateChangeListener;
+
+    /**
+     * Touch mode for the Drawer.
+     * Possible values are {@link #TOUCH_MODE_NONE}, {@link #TOUCH_MODE_BEZEL} or {@link #TOUCH_MODE_FULLSCREEN}
+     * Default: {@link #TOUCH_MODE_BEZEL}
+     */
+    protected int mTouchMode = TOUCH_MODE_BEZEL;
+
+    /**
+     * Indicates whether to use {@link View#LAYER_TYPE_HARDWARE} when animating the drawer.
+     */
+    protected boolean mHardwareLayersEnabled = true;
+
+    /**
+     * The Activity the drawer is attached to.
+     */
+    private Activity mActivity;
+
+    /**
+     * Scroller used when animating the indicator to a new position.
+     */
+    private FloatScroller mIndicatorScroller;
+
+    /**
+     * Runnable used when animating the indicator to a new position.
+     */
+    private Runnable mIndicatorRunnable = new Runnable() {
+        @Override
+        public void run() {
+            animateIndicatorInvalidate();
+        }
+    };
+
+    /**
+     * The start position of the indicator when animating it to a new position.
+     */
+    protected int mIndicatorStartPos;
+
+    /**
+     * [0..1] value indicating the current progress of the animation.
+     */
+    protected float mIndicatorOffset;
+
+    /**
+     * Whether the indicator is currently animating.
+     */
+    protected boolean mIndicatorAnimating;
+
+    /**
+     * Bundle used to hold the drawers state.
+     */
+    protected Bundle mState;
+
+    /**
+     * The maximum duration of open/close animations.
+     */
+    protected int mMaxAnimationDuration = DEFAULT_ANIMATION_DURATION;
+
+    /**
+     * Callback that lets the listener override intercepting of touch events.
+     */
+    protected OnInterceptMoveEventListener mOnInterceptMoveEventListener;
+
+    protected Drawable mWindowEdge = null;
+
+    /**
+     * Attaches the MenuDrawer to the Activity.
+     *
+     * @param activity The activity that the MenuDrawer will be attached to.
+     * @return The created MenuDrawer instance.
+     */
+    public static MenuDrawer attach(Activity activity) {
+        return attach(activity, MENU_DRAG_CONTENT);
+    }
+
+    /**
+     * Attaches the MenuDrawer to the Activity.
+     *
+     * @param activity The activity the menu drawer will be attached to.
+     * @param dragMode The drag mode of the drawer. Can be either {@link MenuDrawer#MENU_DRAG_CONTENT}
+     *                 or {@link MenuDrawer#MENU_DRAG_WINDOW}.
+     * @return The created MenuDrawer instance.
+     */
+    public static MenuDrawer attach(Activity activity, int dragMode) {
+        return attach(activity, dragMode, Position.LEFT);
+    }
+
+    /**
+     * Attaches the MenuDrawer to the Activity.
+     *
+     * @param activity The activity the menu drawer will be attached to.
+     * @param position Where to position the menu.
+     * @return The created MenuDrawer instance.
+     */
+    public static MenuDrawer attach(Activity activity, Position position) {
+        return attach(activity, MENU_DRAG_CONTENT, position);
+    }
+
+    /**
+     * Attaches the MenuDrawer to the Activity.
+     *
+     * @param activity The activity the menu drawer will be attached to.
+     * @param dragMode The drag mode of the drawer. Can be either {@link MenuDrawer#MENU_DRAG_CONTENT}
+     *                 or {@link MenuDrawer#MENU_DRAG_WINDOW}.
+     * @param position Where to position the menu.
+     * @return The created MenuDrawer instance.
+     */
+    public static MenuDrawer attach(Activity activity, int dragMode, Position position) {
+        return attach(activity, dragMode, position, false);
+    }
+
+    /**
+     * Attaches the MenuDrawer to the Activity.
+     *
+     * @param activity     The activity the menu drawer will be attached to.
+     * @param dragMode     The drag mode of the drawer. Can be either {@link MenuDrawer#MENU_DRAG_CONTENT}
+     *                     or {@link MenuDrawer#MENU_DRAG_WINDOW}.
+     * @param position     Where to position the menu.
+     * @param attachStatic Whether a static (non-draggable, always visible) drawer should be used.
+     * @return The created MenuDrawer instance.
+     */
+    public static MenuDrawer attach(Activity activity, int dragMode, Position position, boolean attachStatic) {
+        MenuDrawer menuDrawer = createMenuDrawer(activity, dragMode, position, attachStatic);
+        menuDrawer.setId(R.id.md__drawer);
+
+        switch (dragMode) {
+            case MenuDrawer.MENU_DRAG_CONTENT:
+                attachToContent(activity, menuDrawer);
+                break;
+
+            case MenuDrawer.MENU_DRAG_WINDOW:
+                attachToDecor(activity, menuDrawer);
+                break;
+
+            default:
+                throw new RuntimeException("Unknown menu mode: " + dragMode);
+        }
+
+        return menuDrawer;
+    }
+
+    /**
+     * Constructs the appropriate MenuDrawer based on the position.
+     */
+    private static MenuDrawer createMenuDrawer(Activity activity, int dragMode, Position position,
+            boolean attachStatic) {
+        if (attachStatic) {
+            switch (position) {
+                case LEFT:
+                    return new LeftStaticDrawer(activity, dragMode);
+                case RIGHT:
+                    return new RightStaticDrawer(activity, dragMode);
+                case TOP:
+                    return new TopStaticDrawer(activity, dragMode);
+                case BOTTOM:
+                    return new BottomStaticDrawer(activity, dragMode);
+                default:
+                    throw new IllegalArgumentException("position must be one of LEFT, TOP, RIGHT or BOTTOM");
+            }
+        }
+
+        switch (position) {
+            case LEFT:
+                return new LeftDrawer(activity, dragMode);
+            case RIGHT:
+                return new RightDrawer(activity, dragMode);
+            case TOP:
+                return new TopDrawer(activity, dragMode);
+            case BOTTOM:
+                return new BottomDrawer(activity, dragMode);
+            default:
+                throw new IllegalArgumentException("position must be one of LEFT, TOP, RIGHT or BOTTOM");
+        }
+    }
+
+    /**
+     * Attaches the menu drawer to the content view.
+     */
+    private static void attachToContent(Activity activity, MenuDrawer menuDrawer) {
+        /**
+         * Do not call mActivity#setContentView.
+         * E.g. if using with a ListActivity, Activity#setContentView is overridden and dispatched to
+         * MenuDrawer#setContentView, which then again would call Activity#setContentView.
+         */
+        ViewGroup content = (ViewGroup) activity.findViewById(android.R.id.content);
+        content.removeAllViews();
+        content.addView(menuDrawer, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+    }
+
+    /**
+     * Attaches the menu drawer to the window.
+     */
+    private static void attachToDecor(Activity activity, MenuDrawer menuDrawer) {
+        ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
+        ViewGroup decorChild = (ViewGroup) decorView.getChildAt(0);
+
+        decorView.removeAllViews();
+        decorView.addView(menuDrawer, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+
+        menuDrawer.mContentContainer.addView(decorChild, decorChild.getLayoutParams());
+    }
+
+    MenuDrawer(Activity activity, int dragMode) {
+        this(activity);
+
+        mActivity = activity;
+        mDragMode = dragMode;
+    }
+
+    public MenuDrawer(Context context) {
+        this(context, null);
+    }
+
+    public MenuDrawer(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.menuDrawerStyle);
+    }
+
+    public MenuDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        initDrawer(context, attrs, defStyle);
+    }
+
+    protected void initDrawer(Context context, AttributeSet attrs, int defStyle) {
+        setWillNotDraw(false);
+        setFocusable(false);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MenuDrawer, R.attr.menuDrawerStyle,
+                R.style.Widget_MenuDrawer);
+
+        final Drawable contentBackground = a.getDrawable(R.styleable.MenuDrawer_mdContentBackground);
+        final Drawable menuBackground = a.getDrawable(R.styleable.MenuDrawer_mdMenuBackground);
+
+        mMenuSize = a.getDimensionPixelSize(R.styleable.MenuDrawer_mdMenuSize, -1);
+        mMenuSizeSet = mMenuSize != -1;
+
+        final int indicatorResId = a.getResourceId(R.styleable.MenuDrawer_mdActiveIndicator, 0);
+        if (indicatorResId != 0) {
+            mActiveIndicator = BitmapFactory.decodeResource(getResources(), indicatorResId);
+        }
+
+        mDropShadowEnabled = a.getBoolean(R.styleable.MenuDrawer_mdDropShadowEnabled, true);
+
+        mDropShadowDrawable = a.getDrawable(R.styleable.MenuDrawer_mdDropShadow);
+
+        if (mDropShadowDrawable == null) {
+            final int dropShadowColor = a.getColor(R.styleable.MenuDrawer_mdDropShadowColor, 0xFF000000);
+            setDropShadowColor(dropShadowColor);
+        }
+
+        mDropShadowSize = a.getDimensionPixelSize(R.styleable.MenuDrawer_mdDropShadowSize,
+                dpToPx(DEFAULT_DROP_SHADOW_DP));
+
+        mTouchBezelSize = a.getDimensionPixelSize(R.styleable.MenuDrawer_mdTouchBezelSize,
+                dpToPx(DEFAULT_DRAG_BEZEL_DP));
+
+        mAllowIndicatorAnimation = a.getBoolean(R.styleable.MenuDrawer_mdAllowIndicatorAnimation, false);
+
+        mMaxAnimationDuration = a.getInt(R.styleable.MenuDrawer_mdMaxAnimationDuration, DEFAULT_ANIMATION_DURATION);
+
+        a.recycle();
+
+        mMenuContainer = new BuildLayerFrameLayout(context);
+        mMenuContainer.setId(R.id.md__menu);
+        mMenuContainer.setBackgroundDrawable(null); //TODO Make this modifiable in code
+        super.addView(mMenuContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+
+        mContentContainer = new NoClickThroughFrameLayout(context);
+        mContentContainer.setId(R.id.md__content);
+        mContentContainer.setBackgroundDrawable(contentBackground);
+        super.addView(mContentContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+
+        mMenuOverlay = new ColorDrawable(0xFF000000);
+
+        mIndicatorScroller = new FloatScroller(SMOOTH_INTERPOLATOR);
+    }
+
+    public void setMenuOverlayColor (int color) {
+        mMenuOverlay = new ColorDrawable(color);
+    }
+
+    public void setWindowEdge (Drawable windowEdge) {
+        mWindowEdge = windowEdge;
+    }
+
+    @Override
+    public void addView(View child, int index, LayoutParams params) {
+        int childCount = mMenuContainer.getChildCount();
+        if (childCount == 0) {
+            mMenuContainer.addView(child, index, params);
+            return;
+        }
+
+        childCount = mContentContainer.getChildCount();
+        if (childCount == 0) {
+            mContentContainer.addView(child, index, params);
+            return;
+        }
+
+        throw new IllegalStateException("MenuDrawer can only hold two child views");
+    }
+
+    protected int dpToPx(int dp) {
+        return (int) (getResources().getDisplayMetrics().density * dp + 0.5f);
+    }
+
+    protected boolean isViewDescendant(View v) {
+        ViewParent parent = v.getParent();
+        while (parent != null) {
+            if (parent == this) {
+                return true;
+            }
+
+            parent = parent.getParent();
+        }
+
+        return false;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        getViewTreeObserver().addOnScrollChangedListener(mScrollListener);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        getViewTreeObserver().removeOnScrollChangedListener(mScrollListener);
+        super.onDetachedFromWindow();
+    }
+
+    /**
+     * Toggles the menu open and close with animation.
+     */
+    public void toggleMenu() {
+        toggleMenu(true);
+    }
+
+    /**
+     * Toggles the menu open and close.
+     *
+     * @param animate Whether open/close should be animated.
+     */
+    public abstract void toggleMenu(boolean animate);
+
+    /**
+     * Animates the menu open.
+     */
+    public void openMenu() {
+        openMenu(true);
+    }
+
+    /**
+     * Opens the menu.
+     *
+     * @param animate Whether open/close should be animated.
+     */
+    public abstract void openMenu(boolean animate);
+
+    /**
+     * Animates the menu closed.
+     */
+    public void closeMenu() {
+        closeMenu(true);
+    }
+
+    /**
+     * Closes the menu.
+     *
+     * @param animate Whether open/close should be animated.
+     */
+    public abstract void closeMenu(boolean animate);
+
+    /**
+     * Indicates whether the menu is currently visible.
+     *
+     * @return True if the menu is open, false otherwise.
+     */
+    public abstract boolean isMenuVisible();
+
+    /**
+     * Set the size of the menu drawer when open.
+     *
+     * @param size The size of the menu.
+     */
+    public abstract void setMenuSize(int size);
+
+    /**
+     * Returns the size of the menu.
+     *
+     * @return The size of the menu.
+     */
+    public int getMenuSize() {
+        return mMenuSize;
+    }
+
+    /**
+     * Set the active view.
+     * If the mdActiveIndicator attribute is set, this View will have the indicator drawn next to it.
+     *
+     * @param v The active view.
+     */
+    public void setActiveView(View v) {
+        setActiveView(v, 0);
+    }
+
+    /**
+     * Set the active view.
+     * If the mdActiveIndicator attribute is set, this View will have the indicator drawn next to it.
+     *
+     * @param v        The active view.
+     * @param position Optional position, usually used with ListView. v.setTag(R.id.mdActiveViewPosition, position)
+     *                 must be called first.
+     */
+    public void setActiveView(View v, int position) {
+        final View oldView = mActiveView;
+        mActiveView = v;
+        mActivePosition = position;
+
+        if (mAllowIndicatorAnimation && oldView != null) {
+            startAnimatingIndicator();
+        }
+
+        invalidate();
+    }
+
+    /**
+     * Sets whether the indicator should be animated between active views.
+     *
+     * @param animate Whether the indicator should be animated between active views.
+     */
+    public void setAllowIndicatorAnimation(boolean animate) {
+        if (animate != mAllowIndicatorAnimation) {
+            mAllowIndicatorAnimation = animate;
+            completeAnimatingIndicator();
+        }
+    }
+
+    /**
+     * Indicates whether the indicator should be animated between active views.
+     *
+     * @return Whether the indicator should be animated between active views.
+     */
+    public boolean getAllowIndicatorAnimation() {
+        return mAllowIndicatorAnimation;
+    }
+
+    /**
+     * Scroll listener that checks whether the active view has moved before the drawer is invalidated.
+     */
+    private ViewTreeObserver.OnScrollChangedListener mScrollListener = new ViewTreeObserver.OnScrollChangedListener() {
+        @Override
+        public void onScrollChanged() {
+            if (mActiveView != null && isViewDescendant(mActiveView)) {
+                mActiveView.getDrawingRect(mTempRect);
+                offsetDescendantRectToMyCoords(mActiveView, mTempRect);
+                if (mTempRect.left != mActiveRect.left || mTempRect.top != mActiveRect.top
+                        || mTempRect.right != mActiveRect.right || mTempRect.bottom != mActiveRect.bottom) {
+                    invalidate();
+                }
+            }
+        }
+    };
+
+    /**
+     * Starts animating the indicator to a new position.
+     */
+    private void startAnimatingIndicator() {
+        mIndicatorStartPos = getIndicatorStartPos();
+        mIndicatorAnimating = true;
+        mIndicatorScroller.startScroll(0.0f, 1.0f, INDICATOR_ANIM_DURATION);
+
+        animateIndicatorInvalidate();
+    }
+
+    /**
+     * Returns the start position of the indicator.
+     *
+     * @return The start position of the indicator.
+     */
+    protected abstract int getIndicatorStartPos();
+
+    /**
+     * Callback when each frame in the indicator animation should be drawn.
+     */
+    private void animateIndicatorInvalidate() {
+        if (mIndicatorScroller.computeScrollOffset()) {
+            mIndicatorOffset = mIndicatorScroller.getCurr();
+            invalidate();
+
+            if (!mIndicatorScroller.isFinished()) {
+                postOnAnimation(mIndicatorRunnable);
+                return;
+            }
+        }
+
+        completeAnimatingIndicator();
+    }
+
+    /**
+     * Called when the indicator animation has completed.
+     */
+    private void completeAnimatingIndicator() {
+        mIndicatorOffset = 1.0f;
+        mIndicatorAnimating = false;
+        invalidate();
+    }
+
+    /**
+     * Enables or disables offsetting the menu when dragging the drawer.
+     *
+     * @param offsetMenu True to offset the menu, false otherwise.
+     */
+    public abstract void setOffsetMenuEnabled(boolean offsetMenu);
+
+    /**
+     * Indicates whether the menu is being offset when dragging the drawer.
+     *
+     * @return True if the menu is being offset, false otherwise.
+     */
+    public abstract boolean getOffsetMenuEnabled();
+
+    public int getDrawerState() {
+        return mDrawerState;
+    }
+
+    /**
+     * Register a callback to be invoked when the drawer state changes.
+     *
+     * @param listener The callback that will run.
+     */
+    public void setOnDrawerStateChangeListener(OnDrawerStateChangeListener listener) {
+        mOnDrawerStateChangeListener = listener;
+    }
+
+    /**
+     * Register a callback that will be invoked when the drawer is about to intercept touch events.
+     *
+     * @param listener The callback that will be invoked.
+     */
+    public void setOnInterceptMoveEventListener(OnInterceptMoveEventListener listener) {
+        mOnInterceptMoveEventListener = listener;
+    }
+
+    /**
+     * Defines whether the drop shadow is enabled.
+     *
+     * @param enabled Whether the drop shadow is enabled.
+     */
+    public void setDropShadowEnabled(boolean enabled) {
+        mDropShadowEnabled = enabled;
+        invalidate();
+    }
+
+    /**
+     * Sets the color of the drop shadow.
+     *
+     * @param color The color of the drop shadow.
+     */
+    public abstract void setDropShadowColor(int color);
+
+    /**
+     * Sets the drawable of the drop shadow.
+     *
+     * @param drawable The drawable of the drop shadow.
+     */
+    public void setDropShadow(Drawable drawable) {
+        mDropShadowDrawable = drawable;
+        invalidate();
+    }
+
+    /**
+     * Sets the drawable of the drop shadow.
+     *
+     * @param resId The resource identifier of the the drawable.
+     */
+    public void setDropShadow(int resId) {
+        setDropShadow(getResources().getDrawable(resId));
+    }
+
+    /**
+     * Returns the drawable of the drop shadow.
+     */
+    public Drawable getDropShadow() {
+        return mDropShadowDrawable;
+    }
+
+    /**
+     * Sets the size of the drop shadow.
+     *
+     * @param size The size of the drop shadow in px.
+     */
+    public void setDropShadowSize(int size) {
+        mDropShadowSize = size;
+        invalidate();
+    }
+
+    /**
+     * Animates the drawer slightly open until the user opens the drawer.
+     */
+    public abstract void peekDrawer();
+
+    /**
+     * Animates the drawer slightly open. If delay is larger than 0, this happens until the user opens the drawer.
+     *
+     * @param delay The delay (in milliseconds) between each run of the animation. If 0, this animation is only run
+     *              once.
+     */
+    public abstract void peekDrawer(long delay);
+
+    /**
+     * Animates the drawer slightly open. If delay is larger than 0, this happens until the user opens the drawer.
+     *
+     * @param startDelay The delay (in milliseconds) until the animation is first run.
+     * @param delay      The delay (in milliseconds) between each run of the animation. If 0, this animation is only run
+     *                   once.
+     */
+    public abstract void peekDrawer(long startDelay, long delay);
+
+    /**
+     * Enables or disables the user of {@link View#LAYER_TYPE_HARDWARE} when animations views.
+     *
+     * @param enabled Whether hardware layers are enabled.
+     */
+    public abstract void setHardwareLayerEnabled(boolean enabled);
+
+    /**
+     * Sets the maximum duration of open/close animations.
+     * @param duration The maximum duration in milliseconds.
+     */
+    public void setMaxAnimationDuration(int duration) {
+        mMaxAnimationDuration = duration;
+    }
+
+    /**
+     * Returns the ViewGroup used as a parent for the menu view.
+     *
+     * @return The menu view's parent.
+     */
+    public ViewGroup getMenuContainer() {
+        return mMenuContainer;
+    }
+
+    /**
+     * Returns the ViewGroup used as a parent for the content view.
+     *
+     * @return The content view's parent.
+     */
+    public ViewGroup getContentContainer() {
+        if (mDragMode == MENU_DRAG_CONTENT) {
+            return mContentContainer;
+        } else {
+            return (ViewGroup) findViewById(android.R.id.content);
+        }
+    }
+
+    /**
+     * Set the menu view from a layout resource.
+     *
+     * @param layoutResId Resource ID to be inflated.
+     */
+    public void setMenuView(int layoutResId) {
+        mMenuContainer.removeAllViews();
+        mMenuView = LayoutInflater.from(getContext()).inflate(layoutResId, mMenuContainer, false);
+        mMenuContainer.addView(mMenuView);
+    }
+
+    /**
+     * Set the menu view to an explicit view.
+     *
+     * @param view The menu view.
+     */
+    public void setMenuView(View view) {
+        setMenuView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+    }
+
+    /**
+     * Set the menu view to an explicit view.
+     *
+     * @param view   The menu view.
+     * @param params Layout parameters for the view.
+     */
+    public void setMenuView(View view, LayoutParams params) {
+        mMenuView = view;
+        mMenuContainer.removeAllViews();
+        mMenuContainer.addView(view, params);
+    }
+
+    /**
+     * Returns the menu view.
+     *
+     * @return The menu view.
+     */
+    public View getMenuView() {
+        return mMenuView;
+    }
+
+    /**
+     * Set the content from a layout resource.
+     *
+     * @param layoutResId Resource ID to be inflated.
+     */
+    public void setContentView(int layoutResId) {
+        switch (mDragMode) {
+            case MenuDrawer.MENU_DRAG_CONTENT:
+                mContentContainer.removeAllViews();
+                LayoutInflater.from(getContext()).inflate(layoutResId, mContentContainer, true);
+                break;
+
+            case MenuDrawer.MENU_DRAG_WINDOW:
+                mActivity.setContentView(layoutResId);
+                break;
+        }
+    }
+
+    /**
+     * Set the content to an explicit view.
+     *
+     * @param view The desired content to display.
+     */
+    public void setContentView(View view) {
+        setContentView(view, new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+    }
+
+    /**
+     * Set the content to an explicit view.
+     *
+     * @param view   The desired content to display.
+     * @param params Layout parameters for the view.
+     */
+    public void setContentView(View view, LayoutParams params) {
+        switch (mDragMode) {
+            case MenuDrawer.MENU_DRAG_CONTENT:
+                mContentContainer.removeAllViews();
+                mContentContainer.addView(view, params);
+                break;
+
+            case MenuDrawer.MENU_DRAG_WINDOW:
+                mActivity.setContentView(view, params);
+                break;
+        }
+    }
+
+    protected void setDrawerState(int state) {
+        if (state != mDrawerState) {
+            final int oldState = mDrawerState;
+            mDrawerState = state;
+            if (mOnDrawerStateChangeListener != null) mOnDrawerStateChangeListener.onDrawerStateChange(oldState, state);
+            if (DEBUG) logDrawerState(state);
+        }
+    }
+
+    protected void logDrawerState(int state) {
+        switch (state) {
+            case STATE_CLOSED:
+                Log.d(TAG, "[DrawerState] STATE_CLOSED");
+                break;
+
+            case STATE_CLOSING:
+                Log.d(TAG, "[DrawerState] STATE_CLOSING");
+                break;
+
+            case STATE_DRAGGING:
+                Log.d(TAG, "[DrawerState] STATE_DRAGGING");
+                break;
+
+            case STATE_OPENING:
+                Log.d(TAG, "[DrawerState] STATE_OPENING");
+                break;
+
+            case STATE_OPEN:
+                Log.d(TAG, "[DrawerState] STATE_OPEN");
+                break;
+
+            default:
+                Log.d(TAG, "[DrawerState] Unknown: " + state);
+        }
+    }
+
+    /**
+     * Returns the touch mode.
+     */
+    public abstract int getTouchMode();
+
+    /**
+     * Sets the drawer touch mode. Possible values are {@link #TOUCH_MODE_NONE}, {@link #TOUCH_MODE_BEZEL} or
+     * {@link #TOUCH_MODE_FULLSCREEN}.
+     *
+     * @param mode The touch mode.
+     */
+    public abstract void setTouchMode(int mode);
+
+    /**
+     * Sets the size of the touch bezel.
+     *
+     * @param size The touch bezel size in px.
+     */
+    public abstract void setTouchBezelSize(int size);
+
+    /**
+     * Returns the size of the touch bezel in px.
+     */
+    public abstract int getTouchBezelSize();
+
+    @Override
+    public void postOnAnimation(Runnable action) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+            super.postOnAnimation(action);
+        } else {
+            postDelayed(action, ANIMATION_DELAY);
+        }
+    }
+
+    @Override
+    protected boolean fitSystemWindows(Rect insets) {
+        if (mDragMode == MENU_DRAG_WINDOW) {
+            mMenuContainer.setPadding(0, insets.top, 0, 0);
+        }
+        return super.fitSystemWindows(insets);
+    }
+
+    /**
+     * Saves the state of the drawer.
+     *
+     * @return Returns a Parcelable containing the drawer state.
+     */
+    public final Parcelable saveState() {
+        if (mState == null) mState = new Bundle();
+        saveState(mState);
+        return mState;
+    }
+
+    void saveState(Bundle state) {
+        // State saving isn't required for subclasses.
+    }
+
+    /**
+     * Restores the state of the drawer.
+     *
+     * @param in A parcelable containing the drawer state.
+     */
+    public void restoreState(Parcelable in) {
+        mState = (Bundle) in;
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState state = new SavedState(superState);
+
+        if (mState == null) mState = new Bundle();
+        saveState(mState);
+
+        state.mState = mState;
+        return state;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        SavedState savedState = (SavedState) state;
+        super.onRestoreInstanceState(savedState.getSuperState());
+
+        restoreState(savedState.mState);
+    }
+
+    static class SavedState extends BaseSavedState {
+
+        Bundle mState;
+
+        public SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        public SavedState(Parcel in) {
+            super(in);
+            mState = in.readBundle();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+            dest.writeBundle(mState);
+        }
+
+        @SuppressWarnings("UnusedDeclaration")
+        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
+            @Override
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/NoClickThroughFrameLayout.java b/MenuDrawer/src/net/simonvt/menudrawer/NoClickThroughFrameLayout.java
new file mode 100755
index 0000000..9462282
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/NoClickThroughFrameLayout.java
@@ -0,0 +1,28 @@
+package net.simonvt.menudrawer;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+/**
+ * FrameLayout which doesn't let touch events propagate to views positioned behind it in the view hierarchy.
+ */
+public class NoClickThroughFrameLayout extends BuildLayerFrameLayout {
+
+    public NoClickThroughFrameLayout(Context context) {
+        super(context);
+    }
+
+    public NoClickThroughFrameLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public NoClickThroughFrameLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        return true;
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/PeekInterpolator.java b/MenuDrawer/src/net/simonvt/menudrawer/PeekInterpolator.java
new file mode 100755
index 0000000..d0c7600
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/PeekInterpolator.java
@@ -0,0 +1,28 @@
+package net.simonvt.menudrawer;
+
+import android.view.animation.Interpolator;
+
+public class PeekInterpolator implements Interpolator {
+
+    private static final String TAG = "PeekInterpolator";
+
+    private static final SinusoidalInterpolator SINUSOIDAL_INTERPOLATOR = new SinusoidalInterpolator();
+
+    @Override
+    public float getInterpolation(float input) {
+        float result;
+
+        if (input < 1.f / 3.f) {
+            result = SINUSOIDAL_INTERPOLATOR.getInterpolation(input * 3);
+
+        } else if (input > 2.f / 3.f) {
+            final float val = ((input + 1.f / 3.f) - 1.f) * 3.f;
+            result = 1.f - SINUSOIDAL_INTERPOLATOR.getInterpolation(val);
+
+        } else {
+            result = 1.f;
+        }
+
+        return result;
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/Position.java b/MenuDrawer/src/net/simonvt/menudrawer/Position.java
new file mode 100755
index 0000000..e1a3dd3
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/Position.java
@@ -0,0 +1,18 @@
+package net.simonvt.menudrawer;
+
+/**
+ * Enums used for positioning the drawer.
+ */
+public enum Position {
+    // Positions the drawer to the left of the content.
+    LEFT,
+
+    // Positions the drawer above the content.
+    TOP,
+
+    // Positions the drawer to the right of the content.
+    RIGHT,
+
+    // Positions the drawer below the content.
+    BOTTOM,
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/RightDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/RightDrawer.java
new file mode 100755
index 0000000..54e1cb5
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/RightDrawer.java
@@ -0,0 +1,250 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+public class RightDrawer extends HorizontalDrawer {
+
+    private int mIndicatorTop;
+
+    RightDrawer(Activity activity, int dragMode) {
+        super(activity, dragMode);
+    }
+
+    public RightDrawer(Context context) {
+        super(context);
+    }
+
+    public RightDrawer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public RightDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void openMenu(boolean animate) {
+        animateOffsetTo(-mMenuSize, 0, animate);
+    }
+
+    @Override
+    public void closeMenu(boolean animate) {
+        animateOffsetTo(0, 0, animate);
+    }
+
+    @Override
+    public void setDropShadowColor(int color) {
+        final int endColor = color & 0x00FFFFFF;
+        mDropShadowDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, new int[] {
+                color,
+                endColor,
+        });
+        invalidate();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int width = r - l;
+        final int height = b - t;
+        final int offsetPixels = (int) mOffsetPixels;
+
+        mMenuContainer.layout(width - mMenuSize, 0, width, height);
+        offsetMenu(offsetPixels);
+
+        if (USE_TRANSLATIONS) {
+            mContentContainer.layout(0, 0, width, height);
+        } else {
+            mContentContainer.layout(offsetPixels, 0, width + offsetPixels, height);
+        }
+    }
+
+    /**
+     * Offsets the menu relative to its original position based on the position of the content.
+     *
+     * @param offsetPixels The number of pixels the content if offset.
+     */
+    private void offsetMenu(int offsetPixels) {
+        if (mOffsetMenu && mMenuSize != 0) {
+            final int menuWidth = mMenuSize;
+            final float openRatio = (menuWidth + (float) offsetPixels) / menuWidth;
+
+            if (USE_TRANSLATIONS) {
+                if (offsetPixels != 0) {
+                    final int offset = (int) (/* 0.25f * */ (openRatio * menuWidth)); //TODO Bookmark
+//                    mMenuContainer.setTranslationX(offset);
+                    mMenuContainer.setTranslationX(0);
+                } else {
+//                    mMenuContainer.setTranslationX(-menuWidth);
+                    mMenuContainer.setTranslationX(0);
+                }
+                mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
+            } else {
+                final int width = getWidth();
+                final int oldMenuRight = mMenuContainer.getRight();
+                final int newRight = width + (int) (0.25f * (openRatio * menuWidth));
+                final int offset = newRight - oldMenuRight;
+                mMenuContainer.offsetLeftAndRight(offset);
+                mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
+            }
+        }
+    }
+
+    @Override
+    protected void drawDropShadow(Canvas canvas, int offsetPixels) {
+        final int height = getHeight();
+        final int width = getWidth();
+        final int left = width + offsetPixels;
+        final int right = left + mDropShadowSize;
+
+        mDropShadowDrawable.setBounds(left, 0, right, height);
+        mDropShadowDrawable.draw(canvas);
+    }
+
+    @Override
+    protected void drawMenuOverlay(Canvas canvas, int offsetPixels) {
+        final int height = getHeight();
+        final int width = getWidth();
+        final int left = 0;
+        final int right = width + offsetPixels;
+        final float openRatio = ((float) Math.abs(offsetPixels)) / mMenuSize;
+
+        mMenuOverlay.setBounds(left, 0, right, height);
+        mMenuOverlay.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * (openRatio)));
+        mMenuOverlay.draw(canvas);
+    }
+
+    @Override
+    protected void drawWindowEdge (Canvas canvas, int offsetPixels) {
+        final int width = getWidth();
+        final int height = getHeight();
+        final int edgeWidth = mWindowEdge.getIntrinsicWidth();
+        final int left = width + offsetPixels - edgeWidth;
+        final int right = width + offsetPixels;
+        final float openRatio = ((float) Math.abs(offsetPixels)) / mMenuSize;
+
+        mWindowEdge.setBounds(left, 0, right, height);
+        mWindowEdge.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * (openRatio)));
+        mWindowEdge.draw(canvas);
+    }
+
+    @Override
+    protected void drawIndicator(Canvas canvas, int offsetPixels) {
+        if (mActiveView != null && isViewDescendant(mActiveView)) {
+            Integer position = (Integer) mActiveView.getTag(R.id.mdActiveViewPosition);
+            final int pos = position == null ? 0 : position;
+
+            if (pos == mActivePosition) {
+                final int width = getWidth();
+                final int menuWidth = mMenuSize;
+                final int indicatorWidth = mActiveIndicator.getWidth();
+
+                final int contentRight = width + offsetPixels;
+                final float openRatio = ((float) Math.abs(offsetPixels)) / menuWidth;
+
+                mActiveView.getDrawingRect(mActiveRect);
+                offsetDescendantRectToMyCoords(mActiveView, mActiveRect);
+
+                final float interpolatedRatio = 1.f - INDICATOR_INTERPOLATOR.getInterpolation((1.f - openRatio));
+                final int interpolatedWidth = (int) (indicatorWidth * interpolatedRatio);
+
+                final int indicatorRight = contentRight + interpolatedWidth;
+                final int indicatorLeft = indicatorRight - indicatorWidth;
+
+                if (mIndicatorAnimating) {
+                    final int indicatorFinalTop = mActiveRect.top + ((mActiveRect.height()
+                            - mActiveIndicator.getHeight()) / 2);
+                    final int indicatorStartTop = mIndicatorStartPos;
+                    final int diff = indicatorFinalTop - indicatorStartTop;
+                    final int startOffset = (int) (diff * mIndicatorOffset);
+                    mIndicatorTop = indicatorStartTop + startOffset;
+                } else {
+                    mIndicatorTop = mActiveRect.top + ((mActiveRect.height() - mActiveIndicator.getHeight()) / 2);
+                }
+
+                canvas.save();
+                canvas.clipRect(contentRight, 0, indicatorRight, getHeight());
+                canvas.drawBitmap(mActiveIndicator, indicatorLeft, mIndicatorTop, null);
+                canvas.restore();
+            }
+        }
+    }
+
+    @Override
+    protected int getIndicatorStartPos() {
+        return mIndicatorTop;
+    }
+
+    @Override
+    protected void initPeekScroller() {
+        final int dx = -mMenuSize / 3;
+        mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
+    }
+
+    @Override
+    protected void onOffsetPixelsChanged(int offsetPixels) {
+        if (USE_TRANSLATIONS) {
+            mContentContainer.setTranslationX(offsetPixels);
+            offsetMenu(offsetPixels);
+            invalidate();
+        } else {
+            mContentContainer.offsetLeftAndRight(offsetPixels - mContentContainer.getLeft());
+            offsetMenu(offsetPixels);
+            invalidate();
+        }
+    }
+
+    //////////////////////////////////////////////////////////////////////
+    // Touch handling
+    //////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected boolean isContentTouch(MotionEvent ev) {
+        return ev.getX() < getWidth() + mOffsetPixels;
+    }
+
+    @Override
+    protected boolean onDownAllowDrag(MotionEvent ev) {
+        final int width = getWidth();
+        final int initialMotionX = (int) mInitialMotionX;
+
+        return (!mMenuVisible && initialMotionX >= width - mTouchSize)
+                || (mMenuVisible && initialMotionX <= width + mOffsetPixels);
+    }
+
+    @Override
+    protected boolean onMoveAllowDrag(MotionEvent ev, float diff) {
+        final int width = getWidth();
+        final int initialMotionX = (int) mInitialMotionX;
+
+        return (!mMenuVisible && initialMotionX >= width - mTouchSize && (diff < 0))
+                || (mMenuVisible && initialMotionX <= width + mOffsetPixels);
+    }
+
+    @Override
+    protected void onMoveEvent(float dx) {
+        final float newOffset = Math.max(Math.min(mOffsetPixels + dx, 0), -mMenuSize);
+        setOffsetPixels(newOffset);
+    }
+
+    @Override
+    protected void onUpEvent(MotionEvent ev, boolean isDownOnOutside) {
+        final int offsetPixels = (int) mOffsetPixels;
+        final int width = getWidth();
+
+        if (mIsDragging) {
+            mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+            final int initialVelocity = (int) mVelocityTracker.getXVelocity();
+            mLastMotionX = ev.getX();
+            animateOffsetTo(mVelocityTracker.getXVelocity() > 0 ? 0 : -mMenuSize, initialVelocity, true);
+
+            // Close the menu when content is clicked while the menu is visible.
+        } else if (mMenuVisible && ev.getX() < width + offsetPixels && isDownOnOutside) {
+            closeMenu();
+        }
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/RightStaticDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/RightStaticDrawer.java
new file mode 100755
index 0000000..2027b43
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/RightStaticDrawer.java
@@ -0,0 +1,87 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+
+public class RightStaticDrawer extends StaticDrawer {
+
+    private int mIndicatorTop;
+
+    RightStaticDrawer(Activity activity, int dragMode) {
+        super(activity, dragMode);
+    }
+
+    public RightStaticDrawer(Context context) {
+        super(context);
+    }
+
+    public RightStaticDrawer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public RightStaticDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void initDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super.initDrawer(context, attrs, defStyle);
+        mPosition = Position.RIGHT;
+    }
+
+    @Override
+    public void setDropShadowColor(int color) {
+        final int endColor = color & 0x00FFFFFF;
+        mDropShadowDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, new int[] {
+                color,
+                endColor,
+        });
+        invalidate();
+    }
+
+    @Override
+    protected void drawIndicator(Canvas canvas) {
+        if (mActiveView != null && isViewDescendant(mActiveView)) {
+            Integer position = (Integer) mActiveView.getTag(R.id.mdActiveViewPosition);
+            final int pos = position == null ? 0 : position;
+
+            if (pos == mActivePosition) {
+                final int width = getWidth();
+                final int menuWidth = mMenuSize;
+                final int indicatorWidth = mActiveIndicator.getWidth();
+
+                final int contentRight = width - menuWidth;
+
+                mActiveView.getDrawingRect(mActiveRect);
+                offsetDescendantRectToMyCoords(mActiveView, mActiveRect);
+
+                final int indicatorRight = contentRight + indicatorWidth;
+                final int indicatorLeft = contentRight;
+
+                if (mIndicatorAnimating) {
+                    final int indicatorFinalTop = mActiveRect.top + ((mActiveRect.height()
+                            - mActiveIndicator.getHeight()) / 2);
+                    final int indicatorStartTop = mIndicatorStartPos;
+                    final int diff = indicatorFinalTop - indicatorStartTop;
+                    final int startOffset = (int) (diff * mIndicatorOffset);
+                    mIndicatorTop = indicatorStartTop + startOffset;
+                } else {
+                    mIndicatorTop = mActiveRect.top + ((mActiveRect.height() - mActiveIndicator.getHeight()) / 2);
+                }
+
+                canvas.save();
+                canvas.clipRect(contentRight, 0, indicatorRight, getHeight());
+                canvas.drawBitmap(mActiveIndicator, indicatorLeft, mIndicatorTop, null);
+                canvas.restore();
+            }
+        }
+    }
+
+    @Override
+    protected int getIndicatorStartPos() {
+        return mIndicatorTop;
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/Scroller.java b/MenuDrawer/src/net/simonvt/menudrawer/Scroller.java
new file mode 100755
index 0000000..58f0fc5
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/Scroller.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * 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 net.simonvt.menudrawer;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.os.Build;
+import android.util.FloatMath;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+
+/**
+ * This class encapsulates scrolling.  The duration of the scroll
+ * can be passed in the constructor and specifies the maximum time that
+ * the scrolling animation should take.  Past this time, the scrolling is
+ * automatically moved to its final stage and computeScrollOffset()
+ * will always return false to indicate that scrolling is over.
+ */
+public class Scroller  {
+    private int mMode;
+
+    private int mStartX;
+    private int mStartY;
+    private int mFinalX;
+    private int mFinalY;
+
+    private int mMinX;
+    private int mMaxX;
+    private int mMinY;
+    private int mMaxY;
+
+    private int mCurrX;
+    private int mCurrY;
+    private long mStartTime;
+    private int mDuration;
+    private float mDurationReciprocal;
+    private float mDeltaX;
+    private float mDeltaY;
+    private boolean mFinished;
+    private Interpolator mInterpolator;
+    private boolean mFlywheel;
+
+    private float mVelocity;
+
+    private static final int DEFAULT_DURATION = 250;
+    private static final int SCROLL_MODE = 0;
+    private static final int FLING_MODE = 1;
+
+    private static final float DECELERATION_RATE = (float) (Math.log(0.75) / Math.log(0.9));
+    private static final float ALPHA = 800; // pixels / seconds
+    private static final float START_TENSION = 0.4f; // Tension at start: (0.4 * total T, 1.0 * Distance)
+    private static final float END_TENSION = 1.0f - START_TENSION;
+    private static final int NB_SAMPLES = 100;
+    private static final float[] SPLINE = new float[NB_SAMPLES + 1];
+
+    private float mDeceleration;
+    private final float mPpi;
+
+    static {
+        float xMin = 0.0f;
+        for (int i = 0; i <= NB_SAMPLES; i++) {
+            final float t = (float) i / NB_SAMPLES;
+            float xMax = 1.0f;
+            float x, tx, coef;
+            while (true) {
+                x = xMin + (xMax - xMin) / 2.0f;
+                coef = 3.0f * x * (1.0f - x);
+                tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x;
+                if (Math.abs(tx - t) < 1E-5) break;
+                if (tx > t) xMax = x;
+                else xMin = x;
+            }
+            final float d = coef + x * x * x;
+            SPLINE[i] = d;
+        }
+        SPLINE[NB_SAMPLES] = 1.0f;
+
+        // This controls the viscous fluid effect (how much of it)
+        sViscousFluidScale = 8.0f;
+        // must be set to 1.0 (used in viscousFluid())
+        sViscousFluidNormalize = 1.0f;
+        sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
+    }
+
+    private static float sViscousFluidScale;
+    private static float sViscousFluidNormalize;
+
+    /**
+     * Create a Scroller with the default duration and interpolator.
+     */
+    public Scroller(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Create a Scroller with the specified interpolator. If the interpolator is
+     * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
+     * be in effect for apps targeting Honeycomb or newer.
+     */
+    public Scroller(Context context, Interpolator interpolator) {
+        this(context, interpolator,
+                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
+    }
+
+    /**
+     * Create a Scroller with the specified interpolator. If the interpolator is
+     * null, the default (viscous) interpolator will be used. Specify whether or
+     * not to support progressive "flywheel" behavior in flinging.
+     */
+    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
+        mFinished = true;
+        mInterpolator = interpolator;
+        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
+        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
+        mFlywheel = flywheel;
+    }
+
+    /**
+     * The amount of friction applied to flings. The default value
+     * is {@link android.view.ViewConfiguration#getScrollFriction}.
+     *
+     * @param friction A scalar dimension-less value representing the coefficient of
+     *         friction.
+     */
+    public final void setFriction(float friction) {
+        mDeceleration = computeDeceleration(friction);
+    }
+
+    private float computeDeceleration(float friction) {
+        return SensorManager.GRAVITY_EARTH   // g (m/s^2)
+                      * 39.37f               // inch/meter
+                      * mPpi                 // pixels per inch
+                      * friction;
+    }
+
+    /**
+     *
+     * Returns whether the scroller has finished scrolling.
+     *
+     * @return True if the scroller has finished scrolling, false otherwise.
+     */
+    public final boolean isFinished() {
+        return mFinished;
+    }
+
+    /**
+     * Force the finished field to a particular value.
+     *
+     * @param finished The new finished value.
+     */
+    public final void forceFinished(boolean finished) {
+        mFinished = finished;
+    }
+
+    /**
+     * Returns how long the scroll event will take, in milliseconds.
+     *
+     * @return The duration of the scroll in milliseconds.
+     */
+    public final int getDuration() {
+        return mDuration;
+    }
+
+    /**
+     * Returns the current X offset in the scroll.
+     *
+     * @return The new X offset as an absolute distance from the origin.
+     */
+    public final int getCurrX() {
+        return mCurrX;
+    }
+
+    /**
+     * Returns the current Y offset in the scroll.
+     *
+     * @return The new Y offset as an absolute distance from the origin.
+     */
+    public final int getCurrY() {
+        return mCurrY;
+    }
+
+    /**
+     * Returns the current velocity.
+     *
+     * @return The original velocity less the deceleration. Result may be
+     * negative.
+     */
+    public float getCurrVelocity() {
+        return mVelocity - mDeceleration * timePassed() / 2000.0f;
+    }
+
+    /**
+     * Returns the start X offset in the scroll.
+     *
+     * @return The start X offset as an absolute distance from the origin.
+     */
+    public final int getStartX() {
+        return mStartX;
+    }
+
+    /**
+     * Returns the start Y offset in the scroll.
+     *
+     * @return The start Y offset as an absolute distance from the origin.
+     */
+    public final int getStartY() {
+        return mStartY;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final X offset as an absolute distance from the origin.
+     */
+    public final int getFinalX() {
+        return mFinalX;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final Y offset as an absolute distance from the origin.
+     */
+    public final int getFinalY() {
+        return mFinalY;
+    }
+
+    /**
+     * Call this when you want to know the new location.  If it returns true,
+     * the animation is not yet finished.  loc will be altered to provide the
+     * new location.
+     */
+    public boolean computeScrollOffset() {
+        if (mFinished) {
+            return false;
+        }
+
+        int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+
+        if (timePassed < mDuration) {
+            switch (mMode) {
+            case SCROLL_MODE:
+                float x = timePassed * mDurationReciprocal;
+
+                if (mInterpolator == null)
+                    x = viscousFluid(x);
+                else
+                    x = mInterpolator.getInterpolation(x);
+
+                mCurrX = mStartX + Math.round(x * mDeltaX);
+                mCurrY = mStartY + Math.round(x * mDeltaY);
+                break;
+            case FLING_MODE:
+                final float t = (float) timePassed / mDuration;
+                final int index = (int) (NB_SAMPLES * t);
+                final float tInf = (float) index / NB_SAMPLES;
+                final float tSup = (float) (index + 1) / NB_SAMPLES;
+                final float dInf = SPLINE[index];
+                final float dSup = SPLINE[index + 1];
+                final float distanceCoef = dInf + (t - tInf) / (tSup - tInf) * (dSup - dInf);
+
+                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
+                // Pin to mMinX <= mCurrX <= mMaxX
+                mCurrX = Math.min(mCurrX, mMaxX);
+                mCurrX = Math.max(mCurrX, mMinX);
+
+                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
+                // Pin to mMinY <= mCurrY <= mMaxY
+                mCurrY = Math.min(mCurrY, mMaxY);
+                mCurrY = Math.max(mCurrY, mMinY);
+
+                if (mCurrX == mFinalX && mCurrY == mFinalY) {
+                    mFinished = true;
+                }
+
+                break;
+            }
+        } else {
+            mCurrX = mFinalX;
+            mCurrY = mFinalY;
+            mFinished = true;
+        }
+        return true;
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     * The scroll will use the default value of 250 milliseconds for the
+     * duration.
+     *
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy) {
+        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     *
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     * @param duration Duration of the scroll in milliseconds.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+        mMode = SCROLL_MODE;
+        mFinished = false;
+        mDuration = duration;
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mStartX = startX;
+        mStartY = startY;
+        mFinalX = startX + dx;
+        mFinalY = startY + dy;
+        mDeltaX = dx;
+        mDeltaY = dy;
+        mDurationReciprocal = 1.0f / (float) mDuration;
+    }
+
+    /**
+     * Start scrolling based on a fling gesture. The distance travelled will
+     * depend on the initial velocity of the fling.
+     *
+     * @param startX Starting point of the scroll (X)
+     * @param startY Starting point of the scroll (Y)
+     * @param velocityX Initial velocity of the fling (X) measured in pixels per
+     *        second.
+     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
+     *        second
+     * @param minX Minimum X value. The scroller will not scroll past this
+     *        point.
+     * @param maxX Maximum X value. The scroller will not scroll past this
+     *        point.
+     * @param minY Minimum Y value. The scroller will not scroll past this
+     *        point.
+     * @param maxY Maximum Y value. The scroller will not scroll past this
+     *        point.
+     */
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY) {
+        // Continue a scroll or fling in progress
+        if (mFlywheel && !mFinished) {
+            float oldVel = getCurrVelocity();
+
+            float dx = (float) (mFinalX - mStartX);
+            float dy = (float) (mFinalY - mStartY);
+            float hyp = FloatMath.sqrt(dx * dx + dy * dy);
+
+            float ndx = dx / hyp;
+            float ndy = dy / hyp;
+
+            float oldVelocityX = ndx * oldVel;
+            float oldVelocityY = ndy * oldVel;
+            if (Math.signum(velocityX) == Math.signum(oldVelocityX)
+                    && Math.signum(velocityY) == Math.signum(oldVelocityY)) {
+                velocityX += oldVelocityX;
+                velocityY += oldVelocityY;
+            }
+        }
+
+        mMode = FLING_MODE;
+        mFinished = false;
+
+        float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);
+
+        mVelocity = velocity;
+        final double l = Math.log(START_TENSION * velocity / ALPHA);
+        mDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0)));
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mStartX = startX;
+        mStartY = startY;
+
+        float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
+        float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
+
+        int totalDistance =
+                (int) (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l));
+
+        mMinX = minX;
+        mMaxX = maxX;
+        mMinY = minY;
+        mMaxY = maxY;
+
+        mFinalX = startX + Math.round(totalDistance * coeffX);
+        // Pin to mMinX <= mFinalX <= mMaxX
+        mFinalX = Math.min(mFinalX, mMaxX);
+        mFinalX = Math.max(mFinalX, mMinX);
+
+        mFinalY = startY + Math.round(totalDistance * coeffY);
+        // Pin to mMinY <= mFinalY <= mMaxY
+        mFinalY = Math.min(mFinalY, mMaxY);
+        mFinalY = Math.max(mFinalY, mMinY);
+    }
+
+    static float viscousFluid(float x) {
+        x *= sViscousFluidScale;
+        if (x < 1.0f) {
+            x -= (1.0f - (float) Math.exp(-x));
+        } else {
+            float start = 0.36787944117f;   // 1/e == exp(-1)
+            x = 1.0f - (float) Math.exp(1.0f - x);
+            x = start + x * (1.0f - start);
+        }
+        x *= sViscousFluidNormalize;
+        return x;
+    }
+
+    /**
+     * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+     * aborting the animating cause the scroller to move to the final x and y
+     * position
+     *
+     * @see #forceFinished(boolean)
+     */
+    public void abortAnimation() {
+        mCurrX = mFinalX;
+        mCurrY = mFinalY;
+        mFinished = true;
+    }
+
+    /**
+     * Extend the scroll animation. This allows a running animation to scroll
+     * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
+     *
+     * @param extend Additional time to scroll in milliseconds.
+     * @see #setFinalX(int)
+     * @see #setFinalY(int)
+     */
+    public void extendDuration(int extend) {
+        int passed = timePassed();
+        mDuration = passed + extend;
+        mDurationReciprocal = 1.0f / mDuration;
+        mFinished = false;
+    }
+
+    /**
+     * Returns the time elapsed since the beginning of the scrolling.
+     *
+     * @return The elapsed time in milliseconds.
+     */
+    public int timePassed() {
+        return (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+    }
+
+    /**
+     * Sets the final position (X) for this scroller.
+     *
+     * @param newX The new X offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalY(int)
+     */
+    public void setFinalX(int newX) {
+        mFinalX = newX;
+        mDeltaX = mFinalX - mStartX;
+        mFinished = false;
+    }
+
+    /**
+     * Sets the final position (Y) for this scroller.
+     *
+     * @param newY The new Y offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalX(int)
+     */
+    public void setFinalY(int newY) {
+        mFinalY = newY;
+        mDeltaY = mFinalY - mStartY;
+        mFinished = false;
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isScrollingInDirection(float xvel, float yvel) {
+        return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX)
+                && Math.signum(yvel) == Math.signum(mFinalY - mStartY);
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/SinusoidalInterpolator.java b/MenuDrawer/src/net/simonvt/menudrawer/SinusoidalInterpolator.java
new file mode 100755
index 0000000..e79051a
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/SinusoidalInterpolator.java
@@ -0,0 +1,15 @@
+package net.simonvt.menudrawer;
+
+import android.view.animation.Interpolator;
+
+/**
+ * Interpolator which, when drawn from 0 to 1, looks like half a sine-wave. Used for smoother opening/closing when
+ * peeking at the drawer.
+ */
+public class SinusoidalInterpolator implements Interpolator {
+
+    @Override
+    public float getInterpolation(float input) {
+        return (float) (0.5f + 0.5f * Math.sin(input * Math.PI - Math.PI / 2.f));
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/SmoothInterpolator.java b/MenuDrawer/src/net/simonvt/menudrawer/SmoothInterpolator.java
new file mode 100755
index 0000000..cfdcb69
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/SmoothInterpolator.java
@@ -0,0 +1,12 @@
+package net.simonvt.menudrawer;
+
+import android.view.animation.Interpolator;
+
+public class SmoothInterpolator implements Interpolator {
+
+    @Override
+    public float getInterpolation(float t) {
+        t -= 1.0f;
+        return t * t * t * t * t + 1.0f;
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/StaticDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/StaticDrawer.java
new file mode 100755
index 0000000..879f2c9
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/StaticDrawer.java
@@ -0,0 +1,208 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+
+public abstract class StaticDrawer extends MenuDrawer {
+
+    protected Position mPosition;
+
+    StaticDrawer(Activity activity, int dragMode) {
+        super(activity, dragMode);
+    }
+
+    public StaticDrawer(Context context) {
+        super(context);
+    }
+
+    public StaticDrawer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public StaticDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+        if (mDropShadowEnabled) drawDropShadow(canvas);
+        if (mActiveIndicator != null) drawIndicator(canvas);
+    }
+
+    private void drawDropShadow(Canvas canvas) {
+        final int width = getWidth();
+        final int height = getHeight();
+        final int menuSize = mMenuSize;
+        final int dropShadowSize = mDropShadowSize;
+
+        switch (mPosition) {
+            case LEFT:
+                mDropShadowDrawable.setBounds(menuSize - dropShadowSize, 0, menuSize, height);
+                break;
+
+            case TOP:
+                mDropShadowDrawable.setBounds(0, menuSize - dropShadowSize, width, menuSize);
+                break;
+
+            case RIGHT:
+                mDropShadowDrawable.setBounds(width - menuSize, 0, width - menuSize + dropShadowSize, height);
+                break;
+
+            case BOTTOM:
+                mDropShadowDrawable.setBounds(0, height - menuSize, width, height - menuSize + dropShadowSize);
+                break;
+        }
+
+        mDropShadowDrawable.draw(canvas);
+    }
+
+    protected abstract void drawIndicator(Canvas canvas);
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int width = r - l;
+        final int height = b - t;
+
+        switch (mPosition) {
+            case LEFT:
+                mMenuContainer.layout(0, 0, mMenuSize, height);
+                mContentContainer.layout(mMenuSize, 0, width, height);
+                break;
+
+            case RIGHT:
+                mMenuContainer.layout(width - mMenuSize, 0, width, height);
+                mContentContainer.layout(0, 0, width - mMenuSize, height);
+                break;
+
+            case TOP:
+                mMenuContainer.layout(0, 0, width, mMenuSize);
+                mContentContainer.layout(0, mMenuSize, width, height);
+                break;
+
+            case BOTTOM:
+                mMenuContainer.layout(0, height - mMenuSize, width, height);
+                mContentContainer.layout(0, 0, width, height - mMenuSize);
+                break;
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+        if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
+            throw new IllegalStateException("Must measure with an exact size");
+        }
+
+        final int width = MeasureSpec.getSize(widthMeasureSpec);
+        final int height = MeasureSpec.getSize(heightMeasureSpec);
+
+        if (!mMenuSizeSet) mMenuSize = (int) (height * 0.25f);
+
+        switch (mPosition) {
+            case LEFT:
+            case RIGHT: {
+                final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+
+                final int menuWidth = mMenuSize;
+                final int menuWidthMeasureSpec = MeasureSpec.makeMeasureSpec(menuWidth, MeasureSpec.EXACTLY);
+
+                final int contentWidth = width - menuWidth;
+                final int contentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY);
+
+                mContentContainer.measure(contentWidthMeasureSpec, childHeightMeasureSpec);
+                mMenuContainer.measure(menuWidthMeasureSpec, childHeightMeasureSpec);
+                break;
+            }
+
+            case TOP:
+            case BOTTOM: {
+                final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
+
+                final int menuHeight = mMenuSize;
+                final int menuHeightMeasureSpec = MeasureSpec.makeMeasureSpec(menuHeight, MeasureSpec.EXACTLY);
+
+                final int contentHeight = height - menuHeight;
+                final int contentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY);
+
+                mContentContainer.measure(childWidthMeasureSpec, contentHeightMeasureSpec);
+                mMenuContainer.measure(childWidthMeasureSpec, menuHeightMeasureSpec);
+                break;
+            }
+        }
+
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    public void toggleMenu(boolean animate) {
+    }
+
+    @Override
+    public void openMenu(boolean animate) {
+    }
+
+    @Override
+    public void closeMenu(boolean animate) {
+    }
+
+    @Override
+    public boolean isMenuVisible() {
+        return true;
+    }
+
+    @Override
+    public void setMenuSize(int size) {
+        mMenuSize = size;
+        mMenuSizeSet = true;
+        requestLayout();
+        invalidate();
+    }
+
+    @Override
+    public void setOffsetMenuEnabled(boolean offsetMenu) {
+    }
+
+    @Override
+    public boolean getOffsetMenuEnabled() {
+        return false;
+    }
+
+    @Override
+    public void peekDrawer() {
+    }
+
+    @Override
+    public void peekDrawer(long delay) {
+    }
+
+    @Override
+    public void peekDrawer(long startDelay, long delay) {
+    }
+
+    @Override
+    public void setHardwareLayerEnabled(boolean enabled) {
+    }
+
+    @Override
+    public int getTouchMode() {
+        return TOUCH_MODE_NONE;
+    }
+
+    @Override
+    public void setTouchMode(int mode) {
+    }
+
+    @Override
+    public void setTouchBezelSize(int size) {
+    }
+
+    @Override
+    public int getTouchBezelSize() {
+        return 0;
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/TopDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/TopDrawer.java
new file mode 100755
index 0000000..198bde5
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/TopDrawer.java
@@ -0,0 +1,229 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+public class TopDrawer extends VerticalDrawer {
+
+    private int mIndicatorLeft;
+
+    TopDrawer(Activity activity, int dragMode) {
+        super(activity, dragMode);
+    }
+
+    public TopDrawer(Context context) {
+        super(context);
+    }
+
+    public TopDrawer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public TopDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void openMenu(boolean animate) {
+        animateOffsetTo(mMenuSize, 0, animate);
+    }
+
+    @Override
+    public void closeMenu(boolean animate) {
+        animateOffsetTo(0, 0, animate);
+    }
+
+    @Override
+    public void setDropShadowColor(int color) {
+        final int endColor = color & 0x00FFFFFF;
+        mDropShadowDrawable = new GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP,
+                new int[] {
+                        color,
+                        endColor,
+                });
+        invalidate();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int width = r - l;
+        final int height = b - t;
+        final int offsetPixels = (int) mOffsetPixels;
+
+        mMenuContainer.layout(0, 0, width, mMenuSize);
+        offsetMenu(offsetPixels);
+
+        if (USE_TRANSLATIONS) {
+            mContentContainer.layout(0, 0, width, height);
+        } else {
+            mContentContainer.layout(0, offsetPixels, width, height + offsetPixels);
+        }
+    }
+
+    /**
+     * Offsets the menu relative to its original position based on the position of the content.
+     *
+     * @param offsetPixels The number of pixels the content if offset.
+     */
+    private void offsetMenu(int offsetPixels) {
+        if (mOffsetMenu && mMenuSize != 0) {
+            final int menuSize = mMenuSize;
+            final float openRatio = (menuSize - (float) offsetPixels) / menuSize;
+
+            if (USE_TRANSLATIONS) {
+                if (offsetPixels > 0) {
+                    final int offset = (int) (0.25f * (-openRatio * menuSize));
+                    mMenuContainer.setTranslationY(offset);
+                } else {
+                    mMenuContainer.setTranslationY(-menuSize);
+                }
+
+            } else {
+                final int oldMenuTop = mMenuContainer.getTop();
+                final int offset = (int) (0.25f * (-openRatio * menuSize)) - oldMenuTop;
+                mMenuContainer.offsetTopAndBottom(offset);
+                mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
+            }
+        }
+    }
+
+    @Override
+    protected void drawDropShadow(Canvas canvas, int offsetPixels) {
+        final int width = getWidth();
+
+        mDropShadowDrawable.setBounds(0, offsetPixels - mDropShadowSize, width, offsetPixels);
+        mDropShadowDrawable.draw(canvas);
+    }
+
+    @Override
+    protected void drawMenuOverlay(Canvas canvas, int offsetPixels) {
+        final int width = getWidth();
+        final float openRatio = ((float) offsetPixels) / mMenuSize;
+
+        mMenuOverlay.setBounds(0, 0, width, offsetPixels);
+        mMenuOverlay.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * (1.f - openRatio)));
+        mMenuOverlay.draw(canvas);
+    }
+
+    @Override
+    protected void drawWindowEdge (Canvas canvas, int offsetPixels) {
+        final int width = getWidth();
+        final int edgeHeight = mWindowEdge.getIntrinsicHeight();
+        final int top = offsetPixels;
+        final int bottom = offsetPixels + edgeHeight;
+        final float openRatio = ((float) Math.abs(offsetPixels)) / mMenuSize;
+
+        mWindowEdge.setBounds(0, top, width, bottom);
+        mWindowEdge.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * (openRatio)));
+        mWindowEdge.draw(canvas);
+    }
+
+    @Override
+    protected void drawIndicator(Canvas canvas, int offsetPixels) {
+        if (mActiveView != null && isViewDescendant(mActiveView)) {
+            Integer position = (Integer) mActiveView.getTag(R.id.mdActiveViewPosition);
+            final int pos = position == null ? 0 : position;
+
+            if (pos == mActivePosition) {
+                final int menuHeight = mMenuSize;
+                final int indicatorHeight = mActiveIndicator.getHeight();
+
+                final float openRatio = ((float) offsetPixels) / menuHeight;
+
+                mActiveView.getDrawingRect(mActiveRect);
+                offsetDescendantRectToMyCoords(mActiveView, mActiveRect);
+                final int indicatorWidth = mActiveIndicator.getWidth();
+
+                final float interpolatedRatio = 1.f - INDICATOR_INTERPOLATOR.getInterpolation((1.f - openRatio));
+                final int interpolatedHeight = (int) (indicatorHeight * interpolatedRatio);
+
+                final int indicatorTop = offsetPixels - interpolatedHeight;
+                if (mIndicatorAnimating) {
+                    final int finalLeft = mActiveRect.left + ((mActiveRect.width() - indicatorWidth) / 2);
+                    final int startLeft = mIndicatorStartPos;
+                    final int diff = finalLeft - startLeft;
+                    final int startOffset = (int) (diff * mIndicatorOffset);
+                    mIndicatorLeft = startLeft + startOffset;
+                } else {
+                    mIndicatorLeft = mActiveRect.left + ((mActiveRect.width() - indicatorWidth) / 2);
+                }
+
+                canvas.save();
+                canvas.clipRect(mIndicatorLeft, indicatorTop, mIndicatorLeft + indicatorWidth, offsetPixels);
+                canvas.drawBitmap(mActiveIndicator, mIndicatorLeft, indicatorTop, null);
+                canvas.restore();
+            }
+        }
+    }
+
+    @Override
+    protected int getIndicatorStartPos() {
+        return mIndicatorLeft;
+    }
+
+    @Override
+    protected void initPeekScroller() {
+        final int dx = mMenuSize / 3;
+        mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
+    }
+
+    @Override
+    protected void onOffsetPixelsChanged(int offsetPixels) {
+        if (USE_TRANSLATIONS) {
+            mContentContainer.setTranslationY(offsetPixels);
+            offsetMenu(offsetPixels);
+            invalidate();
+        } else {
+            mContentContainer.offsetTopAndBottom(offsetPixels - mContentContainer.getTop());
+            offsetMenu(offsetPixels);
+            invalidate();
+        }
+    }
+
+    //////////////////////////////////////////////////////////////////////
+    // Touch handling
+    //////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected boolean isContentTouch(MotionEvent ev) {
+        return ev.getY() > mOffsetPixels;
+    }
+
+    @Override
+    protected boolean onDownAllowDrag(MotionEvent ev) {
+        return (!mMenuVisible && mInitialMotionY <= mTouchSize)
+                || (mMenuVisible && mInitialMotionY >= mOffsetPixels);
+    }
+
+    @Override
+    protected boolean onMoveAllowDrag(MotionEvent ev, float diff) {
+        return (!mMenuVisible && mInitialMotionY <= mTouchSize && (diff > 0))
+                || (mMenuVisible && mInitialMotionY >= mOffsetPixels);
+    }
+
+    @Override
+    protected void onMoveEvent(float dx) {
+        setOffsetPixels(Math.min(Math.max(mOffsetPixels + dx, 0), mMenuSize));
+    }
+
+    @Override
+    protected void onUpEvent(MotionEvent ev, boolean isDownOnOutside) {
+        final int offsetPixels = (int) mOffsetPixels;
+
+        if (mIsDragging) {
+            mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+            final int initialVelocity = (int) mVelocityTracker.getXVelocity();
+            mLastMotionY = ev.getY();
+            animateOffsetTo(mVelocityTracker.getYVelocity() > 0 ? mMenuSize : 0, initialVelocity,
+                    true);
+
+            // Close the menu when content is clicked while the menu is visible.
+        } else if (mMenuVisible && ev.getY() > offsetPixels) {
+            closeMenu();
+        }
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/TopStaticDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/TopStaticDrawer.java
new file mode 100755
index 0000000..ac84614
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/TopStaticDrawer.java
@@ -0,0 +1,82 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+
+public class TopStaticDrawer extends StaticDrawer {
+
+    private int mIndicatorLeft;
+
+    TopStaticDrawer(Activity activity, int dragMode) {
+        super(activity, dragMode);
+    }
+
+    public TopStaticDrawer(Context context) {
+        super(context);
+    }
+
+    public TopStaticDrawer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public TopStaticDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void initDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super.initDrawer(context, attrs, defStyle);
+        mPosition = Position.TOP;
+    }
+
+    @Override
+    public void setDropShadowColor(int color) {
+        final int endColor = color & 0x00FFFFFF;
+        mDropShadowDrawable = new GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP, new int[] {
+                color,
+                endColor,
+        });
+        invalidate();
+    }
+
+    @Override
+    protected void drawIndicator(Canvas canvas) {
+        if (mActiveView != null && isViewDescendant(mActiveView)) {
+            Integer position = (Integer) mActiveView.getTag(R.id.mdActiveViewPosition);
+            final int pos = position == null ? 0 : position;
+
+            if (pos == mActivePosition) {
+                final int menuHeight = mMenuSize;
+                final int indicatorHeight = mActiveIndicator.getHeight();
+
+                mActiveView.getDrawingRect(mActiveRect);
+                offsetDescendantRectToMyCoords(mActiveView, mActiveRect);
+                final int indicatorWidth = mActiveIndicator.getWidth();
+
+                final int indicatorTop = menuHeight - indicatorHeight;
+                if (mIndicatorAnimating) {
+                    final int finalLeft = mActiveRect.left + ((mActiveRect.width() - indicatorWidth) / 2);
+                    final int startLeft = mIndicatorStartPos;
+                    final int diff = finalLeft - startLeft;
+                    final int startOffset = (int) (diff * mIndicatorOffset);
+                    mIndicatorLeft = startLeft + startOffset;
+                } else {
+                    mIndicatorLeft = mActiveRect.left + ((mActiveRect.width() - indicatorWidth) / 2);
+                }
+
+                canvas.save();
+                canvas.clipRect(mIndicatorLeft, indicatorTop, mIndicatorLeft + indicatorWidth, menuHeight);
+                canvas.drawBitmap(mActiveIndicator, mIndicatorLeft, indicatorTop, null);
+                canvas.restore();
+            }
+        }
+    }
+
+    @Override
+    protected int getIndicatorStartPos() {
+        return mIndicatorLeft;
+    }
+}
diff --git a/MenuDrawer/src/net/simonvt/menudrawer/VerticalDrawer.java b/MenuDrawer/src/net/simonvt/menudrawer/VerticalDrawer.java
new file mode 100755
index 0000000..2d7999b
--- /dev/null
+++ b/MenuDrawer/src/net/simonvt/menudrawer/VerticalDrawer.java
@@ -0,0 +1,216 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+
+public abstract class VerticalDrawer extends DraggableDrawer {
+
+    VerticalDrawer(Activity activity, int dragMode) {
+        super(activity, dragMode);
+    }
+
+    public VerticalDrawer(Context context) {
+        super(context);
+    }
+
+    public VerticalDrawer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public VerticalDrawer(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+        if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
+            throw new IllegalStateException("Must measure with an exact size");
+        }
+
+        final int width = MeasureSpec.getSize(widthMeasureSpec);
+        final int height = MeasureSpec.getSize(heightMeasureSpec);
+
+        if (!mMenuSizeSet) mMenuSize = (int) (height * 0.25f);
+        if (mOffsetPixels == -1) openMenu(false);
+
+        final int menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);
+        final int menuHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, mMenuSize);
+        mMenuContainer.measure(menuWidthMeasureSpec, menuHeightMeasureSpec);
+
+        final int contentWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);
+        final int contentHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height);
+        mContentContainer.measure(contentWidthMeasureSpec, contentHeightMeasureSpec);
+
+        setMeasuredDimension(width, height);
+
+        updateTouchAreaSize();
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+        if (action == MotionEvent.ACTION_DOWN && mMenuVisible && isCloseEnough()) {
+            setOffsetPixels(0);
+            stopAnimation();
+            endPeek();
+            setDrawerState(STATE_CLOSED);
+        }
+
+        // Always intercept events over the content while menu is visible.
+        if (mMenuVisible && isContentTouch(ev)) {
+            return true;
+        }
+
+        if (mTouchMode == TOUCH_MODE_NONE) {
+            return false;
+        }
+
+        if (action != MotionEvent.ACTION_DOWN) {
+            if (mIsDragging) {
+                return true;
+            }
+        }
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                mLastMotionX = mInitialMotionX = ev.getX();
+                mLastMotionY = mInitialMotionY = ev.getY();
+                final boolean allowDrag = onDownAllowDrag(ev);
+
+                if (allowDrag) {
+                    setDrawerState(mMenuVisible ? STATE_OPEN : STATE_CLOSED);
+                    stopAnimation();
+                    endPeek();
+                    mIsDragging = false;
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_MOVE: {
+                final float x = ev.getX();
+                final float dx = x - mLastMotionX;
+                final float xDiff = Math.abs(dx);
+                final float y = ev.getY();
+                final float dy = y - mLastMotionY;
+                final float yDiff = Math.abs(dy);
+
+                if (yDiff > mTouchSlop && yDiff > xDiff) {
+                    if (mOnInterceptMoveEventListener != null && mTouchMode == TOUCH_MODE_FULLSCREEN
+                            && canChildScrollVertically(mContentContainer, false, (int) dx, (int) x, (int) y)) {
+                        endDrag(); // Release the velocity tracker
+                        return false;
+                    }
+
+                    final boolean allowDrag = onMoveAllowDrag(ev, dy);
+
+                    if (allowDrag) {
+                        setDrawerState(STATE_DRAGGING);
+                        mIsDragging = true;
+                        mLastMotionX = x;
+                        mLastMotionY = y;
+                    }
+                }
+                break;
+            }
+
+            /**
+             * If you click really fast, an up or cancel event is delivered here. Just snap content to
+             * whatever is closest.
+             */
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP: {
+                if (Math.abs(mOffsetPixels) > mMenuSize / 2) {
+                    openMenu();
+                } else {
+                    closeMenu();
+                }
+                break;
+            }
+        }
+
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(ev);
+
+        return mIsDragging;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (!mMenuVisible && !mIsDragging && (mTouchMode == TOUCH_MODE_NONE)) {
+            return false;
+        }
+        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(ev);
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                mLastMotionX = mInitialMotionX = ev.getX();
+                mLastMotionY = mInitialMotionY = ev.getY();
+                final boolean allowDrag = onDownAllowDrag(ev);
+
+                if (allowDrag) {
+                    stopAnimation();
+                    endPeek();
+                    startLayerTranslation();
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_MOVE: {
+                if (!mIsDragging) {
+                    final float x = ev.getX();
+                    final float dx = x - mLastMotionX;
+                    final float xDiff = Math.abs(dx);
+                    final float y = ev.getY();
+                    final float dy = y - mLastMotionY;
+                    final float yDiff = Math.abs(dy);
+
+                    if (yDiff > mTouchSlop && yDiff > xDiff) {
+                        final boolean allowDrag = onMoveAllowDrag(ev, dy);
+
+                        if (allowDrag) {
+                            setDrawerState(STATE_DRAGGING);
+                            mIsDragging = true;
+                            mLastMotionY = y - mInitialMotionY > 0
+                                    ? mInitialMotionY + mTouchSlop
+                                    : mInitialMotionY - mTouchSlop;
+                        }
+                    }
+                }
+
+                if (mIsDragging) {
+                    startLayerTranslation();
+
+                    final float y = ev.getY();
+                    final float dy = y - mLastMotionY;
+
+                    mLastMotionY = y;
+                    onMoveEvent(dy);
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP: {
+                onUpEvent(ev, true);
+                break;
+            }
+        }
+
+        return true;
+    }
+
+}