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><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;
+ }
+
+}