+ * If it's not posted twice the layer either wont be built on start, or it'll be built twice.
+ */
+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/main/java/net/simonvt/menudrawer/ColorDrawable.java b/menudrawer/src/main/java/net/simonvt/menudrawer/ColorDrawable.java
new file mode 100644
index 00000000..65ca75bf
--- /dev/null
+++ b/menudrawer/src/main/java/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.
+ *
+ *
It can be defined in an XML file with the <color> element.
+ *
+ * @attr ref android.R.styleable#ColorDrawable_color
+ */
+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/main/java/net/simonvt/menudrawer/DraggableDrawer.java b/menudrawer/src/main/java/net/simonvt/menudrawer/DraggableDrawer.java
new file mode 100644
index 00000000..a2f78976
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/DraggableDrawer.java
@@ -0,0 +1,619 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+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 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 = 185;
+
+ /**
+ * 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;
+
+ protected static final int INVALID_POINTER = -1;
+
+ /**
+ * 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();
+ }
+ };
+
+ /**
+ * Indicates whether the drawer is currently being dragged.
+ */
+ protected boolean mIsDragging;
+
+ /**
+ * The current pointer id.
+ */
+ protected int mActivePointerId = INVALID_POINTER;
+
+ /**
+ * 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}.
+ */
+ protected boolean mLayerTypeHardware;
+
+ 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;
+ 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 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;
+ }
+
+ /**
+ * 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}.
+ */
+ protected void stopLayerTranslation() {
+ if (mLayerTypeHardware) {
+ mLayerTypeHardware = false;
+ mContentContainer.setLayerType(View.LAYER_TYPE_NONE, null);
+ mMenuContainer.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ }
+
+ /**
+ * 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();
+ }
+
+ protected void cancelContentTouch() {
+ final long now = SystemClock.uptimeMillis();
+ final MotionEvent cancelEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).dispatchTouchEvent(cancelEvent);
+ }
+ mContentContainer.dispatchTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ }
+
+ /**
+ * 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);
+ animateOffsetTo(position, duration);
+ }
+
+ protected void animateOffsetTo(int position, int duration) {
+ final int startX = (int) mOffsetPixels;
+ final int dx = position - startX;
+
+ 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;
+ }
+
+ protected boolean canChildrenScroll(int dx, int dy, int x, int y) {
+ boolean canScroll = false;
+
+ switch (getPosition()) {
+ case LEFT:
+ case RIGHT:
+ if (!mMenuVisible) {
+ canScroll = canChildScrollHorizontally(mContentContainer, false, dx,
+ x - ViewHelper.getLeft(mContentContainer), y - ViewHelper.getTop(mContentContainer));
+ } else {
+ canScroll = canChildScrollHorizontally(mMenuContainer, false, dx,
+ x - ViewHelper.getLeft(mMenuContainer), y - ViewHelper.getTop(mContentContainer));
+ }
+ break;
+
+ case TOP:
+ case BOTTOM:
+ if (!mMenuVisible) {
+ canScroll = canChildScrollVertically(mContentContainer, false, dy,
+ x - ViewHelper.getLeft(mContentContainer), y - ViewHelper.getTop(mContentContainer));
+ } else {
+ canScroll = canChildScrollVertically(mMenuContainer, false, dy,
+ x - ViewHelper.getLeft(mMenuContainer), y - ViewHelper.getTop(mContentContainer));
+ }
+ }
+
+ return canScroll;
+ }
+
+ /**
+ * 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);
+ }
+
+ protected float getXVelocity(VelocityTracker velocityTracker) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
+ return velocityTracker.getXVelocity(mActivePointerId);
+ }
+
+ return velocityTracker.getXVelocity();
+ }
+
+ protected float getYVelocity(VelocityTracker velocityTracker) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
+ return velocityTracker.getYVelocity(mActivePointerId);
+ }
+
+ return velocityTracker.getYVelocity();
+ }
+
+ 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;
+ }
+
+ 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/main/java/net/simonvt/menudrawer/FloatScroller.java b/menudrawer/src/main/java/net/simonvt/menudrawer/FloatScroller.java
new file mode 100644
index 00000000..42a7babd
--- /dev/null
+++ b/menudrawer/src/main/java/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.
+ */
+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/main/java/net/simonvt/menudrawer/MenuDrawer.java b/menudrawer/src/main/java/net/simonvt/menudrawer/MenuDrawer.java
new file mode 100644
index 00000000..655fcc6e
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/MenuDrawer.java
@@ -0,0 +1,1652 @@
+package net.simonvt.menudrawer;
+
+import net.simonvt.menudrawer.compat.ActionBarHelper;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+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.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.animation.AccelerateInterpolator;
+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);
+
+ /**
+ * Called when the drawer slides.
+ *
+ * @param openRatio Ratio for how open the menu is.
+ * @param offsetPixels Current offset of the menu in pixels.
+ */
+ void onDrawerSlide(float openRatio, int offsetPixels);
+ }
+
+ /**
+ * 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 delta 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 delta, int x, int y);
+ }
+
+ public enum Type {
+ /**
+ * Positions the drawer behind the content.
+ */
+ BEHIND,
+
+ /**
+ * A static drawer that can not be dragged.
+ */
+ STATIC,
+
+ /**
+ * Positions the drawer on top of the content.
+ */
+ OVERLAY,
+ }
+
+ /**
+ * 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.ICE_CREAM_SANDWICH;
+
+ /**
+ * 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();
+
+ /**
+ * Interpolator used for stretching/retracting the active indicator.
+ */
+ protected static final Interpolator INDICATOR_INTERPOLATOR = new AccelerateInterpolator();
+
+ /**
+ * Drawable used as menu overlay.
+ */
+ protected Drawable mMenuOverlay;
+
+ /**
+ * Defines whether the drop shadow is enabled.
+ */
+ protected boolean mDropShadowEnabled;
+
+ /**
+ * The color of the drop shadow.
+ */
+ protected int mDropShadowColor;
+
+ /**
+ * Drawable used as content drop shadow onto the menu.
+ */
+ protected Drawable mDropShadowDrawable;
+
+ private boolean mCustomDropShadow;
+
+ /**
+ * 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 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 SlideDrawable mSlideDrawable;
+
+ protected Drawable mThemeUpIndicator;
+
+ protected boolean mDrawerIndicatorEnabled;
+
+ private ActionBarHelper mActionBarHelper;
+
+ private int mCurrentUpContentDesc;
+
+ private int mDrawerOpenContentDesc;
+
+ private int mDrawerClosedContentDesc;
+
+ /**
+ * The position of the drawer.
+ */
+ private Position mPosition;
+
+ private Position mResolvedPosition;
+
+ private final Rect mIndicatorClipRect = new Rect();
+
+ protected boolean mIsStatic;
+
+ protected final Rect mDropShadowRect = new Rect();
+
+ /**
+ * Current offset.
+ */
+ protected float mOffsetPixels;
+
+ /**
+ * Whether an overlay should be drawn as the drawer is opened and closed.
+ */
+ protected boolean mDrawOverlay;
+
+ /**
+ * 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, Type.BEHIND);
+ }
+
+ /**
+ * Attaches the MenuDrawer to the Activity.
+ *
+ * @param activity The activity the menu drawer will be attached to.
+ * @param type The {@link Type} of the drawer.
+ * @return The created MenuDrawer instance.
+ */
+ public static MenuDrawer attach(Activity activity, Type type) {
+ return attach(activity, type, Position.START);
+ }
+
+ /**
+ * 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, Type.BEHIND, position);
+ }
+
+ /**
+ * Attaches the MenuDrawer to the Activity.
+ *
+ * @param activity The activity the menu drawer will be attached to.
+ * @param type The {@link Type} of the drawer.
+ * @param position Where to position the menu.
+ * @return The created MenuDrawer instance.
+ */
+ public static MenuDrawer attach(Activity activity, Type type, Position position) {
+ return attach(activity, type, position, MENU_DRAG_CONTENT);
+ }
+
+ /**
+ * Attaches the MenuDrawer to the Activity.
+ *
+ * @param activity The activity the menu drawer will be attached to.
+ * @param type The {@link Type} of the drawer.
+ * @param position Where to position the menu.
+ * @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, Type type, Position position, int dragMode) {
+ MenuDrawer menuDrawer = createMenuDrawer(activity, dragMode, position, type);
+ 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, Type type) {
+ MenuDrawer drawer;
+
+ if (type == Type.STATIC) {
+ drawer = new StaticDrawer(activity);
+
+ } else if (type == Type.OVERLAY) {
+ drawer = new OverlayDrawer(activity, dragMode);
+ if (position == Position.LEFT || position == Position.START) {
+ drawer.setupUpIndicator(activity);
+ }
+
+ } else {
+ drawer = new SlidingDrawer(activity, dragMode);
+ if (position == Position.LEFT || position == Position.START) {
+ drawer.setupUpIndicator(activity);
+ }
+ }
+
+ drawer.mDragMode = dragMode;
+ drawer.setPosition(position);
+
+ return drawer;
+ }
+
+ /**
+ * 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, dpToPx(240));
+
+ 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) {
+ mDropShadowColor = a.getColor(R.styleable.MenuDrawer_mdDropShadowColor, 0xFF000000);
+ } else {
+ mCustomDropShadow = true;
+ }
+
+ 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);
+
+ final int slideDrawableResId = a.getResourceId(R.styleable.MenuDrawer_mdSlideDrawable, -1);
+ if (slideDrawableResId != -1) {
+ setSlideDrawable(slideDrawableResId);
+ }
+
+ mDrawerOpenContentDesc = a.getResourceId(R.styleable.MenuDrawer_mdDrawerOpenUpContentDescription, 0);
+ mDrawerClosedContentDesc = a.getResourceId(R.styleable.MenuDrawer_mdDrawerClosedUpContentDescription, 0);
+
+ mDrawOverlay = a.getBoolean(R.styleable.MenuDrawer_mdDrawOverlay, true);
+
+ final int position = a.getInt(R.styleable.MenuDrawer_mdPosition, 0);
+ setPosition(Position.fromValue(position));
+
+ a.recycle();
+
+ mMenuContainer = new NoClickThroughFrameLayout(context);
+ mMenuContainer.setId(R.id.md__menu);
+ mMenuContainer.setBackgroundDrawable(menuBackground);
+
+ mContentContainer = new NoClickThroughFrameLayout(context);
+ mContentContainer.setId(R.id.md__content);
+ mContentContainer.setBackgroundDrawable(contentBackground);
+
+ mMenuOverlay = new ColorDrawable(0xFF000000);
+
+ mIndicatorScroller = new FloatScroller(SMOOTH_INTERPOLATOR);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ View menu = findViewById(R.id.mdMenu);
+ if (menu != null) {
+ removeView(menu);
+ setMenuView(menu);
+ }
+
+ View content = findViewById(R.id.mdContent);
+ if (content != null) {
+ removeView(content);
+ setContentView(content);
+ }
+
+ if (getChildCount() > 2) {
+ throw new IllegalStateException(
+ "Menu and content view added in xml must have id's @id/mdMenu and @id/mdContent");
+ }
+ }
+
+ 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();
+ }
+
+ private boolean shouldDrawIndicator() {
+ return mActiveView != null && mActiveIndicator != null && isViewDescendant(mActiveView);
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+ final int offsetPixels = (int) mOffsetPixels;
+
+ if (mDrawOverlay && offsetPixels != 0) {
+ drawOverlay(canvas);
+ }
+ if (mDropShadowEnabled && (offsetPixels != 0 || mIsStatic)) {
+ drawDropShadow(canvas);
+ }
+ if (shouldDrawIndicator() && (offsetPixels != 0 || mIsStatic)) {
+ drawIndicator(canvas);
+ }
+ }
+
+ protected abstract void drawOverlay(Canvas canvas);
+
+ private void drawDropShadow(Canvas canvas) {
+ // Can't pass the position to the constructor, so wait with loading the drawable until the drop shadow is
+ // actually drawn.
+ if (mDropShadowDrawable == null) {
+ setDropShadowColor(mDropShadowColor);
+ }
+
+ updateDropShadowRect();
+ mDropShadowDrawable.setBounds(mDropShadowRect);
+ mDropShadowDrawable.draw(canvas);
+ }
+
+ protected void updateDropShadowRect() {
+ // This updates the rect for the static and sliding drawer. The overlay drawer has its own implementation.
+ switch (getPosition()) {
+ case LEFT:
+ mDropShadowRect.top = 0;
+ mDropShadowRect.bottom = getHeight();
+ mDropShadowRect.right = ViewHelper.getLeft(mContentContainer);
+ mDropShadowRect.left = mDropShadowRect.right - mDropShadowSize;
+ break;
+
+ case TOP:
+ mDropShadowRect.left = 0;
+ mDropShadowRect.right = getWidth();
+ mDropShadowRect.bottom = ViewHelper.getTop(mContentContainer);
+ mDropShadowRect.top = mDropShadowRect.bottom - mDropShadowSize;
+ break;
+
+ case RIGHT:
+ mDropShadowRect.top = 0;
+ mDropShadowRect.bottom = getHeight();
+ mDropShadowRect.left = ViewHelper.getRight(mContentContainer);
+ mDropShadowRect.right = mDropShadowRect.left + mDropShadowSize;
+ break;
+
+ case BOTTOM:
+ mDropShadowRect.left = 0;
+ mDropShadowRect.right = getWidth();
+ mDropShadowRect.top = ViewHelper.getBottom(mContentContainer);
+ mDropShadowRect.bottom = mDropShadowRect.top + mDropShadowSize;
+ break;
+ }
+ }
+
+ private void drawIndicator(Canvas canvas) {
+ Integer position = (Integer) mActiveView.getTag(R.id.mdActiveViewPosition);
+ final int pos = position == null ? 0 : position;
+ if (pos == mActivePosition) {
+ updateIndicatorClipRect();
+ canvas.save();
+ canvas.clipRect(mIndicatorClipRect);
+
+ int drawLeft = 0;
+ int drawTop = 0;
+ switch (getPosition()) {
+ case LEFT:
+ case TOP:
+ drawLeft = mIndicatorClipRect.left;
+ drawTop = mIndicatorClipRect.top;
+ break;
+
+ case RIGHT:
+ drawLeft = mIndicatorClipRect.right - mActiveIndicator.getWidth();
+ drawTop = mIndicatorClipRect.top;
+ break;
+
+ case BOTTOM:
+ drawLeft = mIndicatorClipRect.left;
+ drawTop = mIndicatorClipRect.bottom - mActiveIndicator.getHeight();
+ }
+
+ canvas.drawBitmap(mActiveIndicator, drawLeft, drawTop, null);
+ canvas.restore();
+ }
+ }
+
+ /**
+ * Update the {@link Rect} where the indicator is drawn.
+ */
+ protected void updateIndicatorClipRect() {
+ mActiveView.getDrawingRect(mActiveRect);
+ offsetDescendantRectToMyCoords(mActiveView, mActiveRect);
+
+ final float openRatio = mIsStatic ? 1.0f : Math.abs(mOffsetPixels) / mMenuSize;
+
+ final float interpolatedRatio = 1.f - INDICATOR_INTERPOLATOR.getInterpolation((1.f - openRatio));
+
+ final int indicatorWidth = mActiveIndicator.getWidth();
+ final int indicatorHeight = mActiveIndicator.getHeight();
+
+ final int interpolatedWidth = (int) (indicatorWidth * interpolatedRatio);
+ final int interpolatedHeight = (int) (indicatorHeight * interpolatedRatio);
+
+ final int startPos = mIndicatorStartPos;
+
+ int left = 0;
+ int top = 0;
+ int right = 0;
+ int bottom = 0;
+
+ switch (getPosition()) {
+ case LEFT:
+ case RIGHT:
+ final int finalTop = mActiveRect.top + ((mActiveRect.height() - indicatorHeight) / 2);
+ if (mIndicatorAnimating) {
+ top = (int) (startPos + ((finalTop - startPos) * mIndicatorOffset));
+ } else {
+ top = finalTop;
+ }
+ bottom = top + indicatorHeight;
+ break;
+
+ case TOP:
+ case BOTTOM:
+ final int finalLeft = mActiveRect.left + ((mActiveRect.width() - indicatorWidth) / 2);
+ if (mIndicatorAnimating) {
+ left = (int) (startPos + ((finalLeft - startPos) * mIndicatorOffset));
+ } else {
+ left = finalLeft;
+ }
+ right = left + indicatorWidth;
+ break;
+ }
+
+ switch (getPosition()) {
+ case LEFT: {
+ right = ViewHelper.getLeft(mContentContainer);
+ left = right - interpolatedWidth;
+ break;
+ }
+
+ case TOP: {
+ bottom = ViewHelper.getTop(mContentContainer);
+ top = bottom - interpolatedHeight;
+ break;
+ }
+
+ case RIGHT: {
+ left = ViewHelper.getRight(mContentContainer);
+ right = left + interpolatedWidth;
+ break;
+ }
+
+ case BOTTOM: {
+ top = ViewHelper.getBottom(mContentContainer);
+ bottom = top + interpolatedHeight;
+ break;
+ }
+ }
+
+ mIndicatorClipRect.left = left;
+ mIndicatorClipRect.top = top;
+ mIndicatorClipRect.right = right;
+ mIndicatorClipRect.bottom = bottom;
+ }
+
+ private void setPosition(Position position) {
+ mPosition = position;
+ mResolvedPosition = getPosition();
+ }
+
+ protected Position getPosition() {
+ final int layoutDirection = ViewHelper.getLayoutDirection(this);
+
+ switch (mPosition) {
+ case START:
+ if (layoutDirection == LAYOUT_DIRECTION_RTL) {
+ return Position.RIGHT;
+ } else {
+ return Position.LEFT;
+ }
+
+ case END:
+ if (layoutDirection == LAYOUT_DIRECTION_RTL) {
+ return Position.LEFT;
+ } else {
+ return Position.RIGHT;
+ }
+ }
+
+ return mPosition;
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+
+ if (!mCustomDropShadow) setDropShadowColor(mDropShadowColor);
+
+ if (getPosition() != mResolvedPosition) {
+ mResolvedPosition = getPosition();
+ setOffsetPixels(mOffsetPixels * -1);
+ }
+
+ if (mSlideDrawable != null) mSlideDrawable.setIsRtl(layoutDirection == LAYOUT_DIRECTION_RTL);
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * 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 (mSlideDrawable != null) {
+ final float offset = Math.abs(mOffsetPixels) / mMenuSize;
+ mSlideDrawable.setOffset(offset);
+ updateUpContentDescription();
+ }
+
+ if (newOffset != oldOffset) {
+ onOffsetPixelsChanged(newOffset);
+ mMenuVisible = newOffset != 0;
+
+ // Notify any attached listeners of the current open ratio
+ final float openRatio = ((float) Math.abs(newOffset)) / mMenuSize;
+ dispatchOnDrawerSlide(openRatio, newOffset);
+ }
+ }
+
+ /**
+ * 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);
+
+ /**
+ * 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.
+ */
+ private int getIndicatorStartPos() {
+ switch (getPosition()) {
+ case TOP:
+ return mIndicatorClipRect.left;
+ case RIGHT:
+ return mIndicatorClipRect.top;
+ case BOTTOM:
+ return mIndicatorClipRect.left;
+ default:
+ return mIndicatorClipRect.top;
+ }
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ /**
+ * 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();
+
+ /**
+ * Get the current state of the drawer.
+ *
+ * @return The state of the drawer.
+ */
+ 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();
+ }
+
+ protected GradientDrawable.Orientation getDropShadowOrientation() {
+ // Gets the orientation for the static and sliding drawer. The overlay drawer provides its own implementation.
+ switch (getPosition()) {
+ case TOP:
+ return GradientDrawable.Orientation.BOTTOM_TOP;
+
+ case RIGHT:
+ return GradientDrawable.Orientation.LEFT_RIGHT;
+
+ case BOTTOM:
+ return GradientDrawable.Orientation.TOP_BOTTOM;
+
+ default:
+ return GradientDrawable.Orientation.RIGHT_LEFT;
+ }
+ }
+
+ /**
+ * Sets the color of the drop shadow.
+ *
+ * @param color The color of the drop shadow.
+ */
+ public void setDropShadowColor(int color) {
+ GradientDrawable.Orientation orientation = getDropShadowOrientation();
+
+ final int endColor = color & 0x00FFFFFF;
+ mDropShadowDrawable = new GradientDrawable(orientation,
+ new int[] {
+ color,
+ endColor,
+ });
+ invalidate();
+ }
+
+ /**
+ * Sets the drawable of the drop shadow.
+ *
+ * @param drawable The drawable of the drop shadow.
+ */
+ public void setDropShadow(Drawable drawable) {
+ mDropShadowDrawable = drawable;
+ mCustomDropShadow = drawable != null;
+ 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;
+ }
+
+ /**
+ * Sets whether an overlay should be drawn when sliding the drawer.
+ *
+ * @param drawOverlay Whether an overlay should be drawn when sliding the drawer.
+ */
+ public void setDrawOverlay(boolean drawOverlay) {
+ mDrawOverlay = drawOverlay;
+ }
+
+ /**
+ * Gets whether an overlay is drawn when sliding the drawer.
+ *
+ * @return Whether an overlay is drawn when sliding the drawer.
+ */
+ public boolean getDrawOverlay() {
+ return mDrawOverlay;
+ }
+
+ protected void updateUpContentDescription() {
+ final int upContentDesc = isMenuVisible() ? mDrawerOpenContentDesc : mDrawerClosedContentDesc;
+ if (mDrawerIndicatorEnabled && mActionBarHelper != null && upContentDesc != mCurrentUpContentDesc) {
+ mCurrentUpContentDesc = upContentDesc;
+ mActionBarHelper.setActionBarDescription(upContentDesc);
+ }
+ }
+
+ /**
+ * Sets the drawable used as the drawer indicator.
+ *
+ * @param drawable The drawable used as the drawer indicator.
+ */
+ public void setSlideDrawable(int drawableRes) {
+ setSlideDrawable(getResources().getDrawable(drawableRes));
+ }
+
+ /**
+ * Sets the drawable used as the drawer indicator.
+ *
+ * @param drawable The drawable used as the drawer indicator.
+ */
+ public void setSlideDrawable(Drawable drawable) {
+ mSlideDrawable = new SlideDrawable(drawable);
+ mSlideDrawable.setIsRtl(ViewHelper.getLayoutDirection(this) == LAYOUT_DIRECTION_RTL);
+
+ if (mActionBarHelper != null) {
+ mActionBarHelper.setDisplayShowHomeAsUpEnabled(true);
+
+ if (mDrawerIndicatorEnabled) {
+ mActionBarHelper.setActionBarUpIndicator(mSlideDrawable,
+ isMenuVisible() ? mDrawerOpenContentDesc : mDrawerClosedContentDesc);
+ }
+ }
+ }
+
+ /**
+ * Sets up the drawer indicator. It cna then be shown with {@link #setDrawerIndicatorEnabled(boolean)}.
+ *
+ * @param activity The activity the drawer is attached to.
+ */
+ public void setupUpIndicator(Activity activity) {
+ if (mActionBarHelper == null) {
+ mActionBarHelper = new ActionBarHelper(activity);
+ mThemeUpIndicator = mActionBarHelper.getThemeUpIndicator();
+
+ if (mDrawerIndicatorEnabled) {
+ mActionBarHelper.setActionBarUpIndicator(mSlideDrawable,
+ isMenuVisible() ? mDrawerOpenContentDesc : mDrawerClosedContentDesc);
+ }
+ }
+ }
+
+ /**
+ * Sets whether the drawer indicator should be enabled. {@link #setupUpIndicator(android.app.Activity)} must be
+ * called first.
+ *
+ * @param enabled Whether the drawer indicator should enabled.
+ */
+ public void setDrawerIndicatorEnabled(boolean enabled) {
+ if (mActionBarHelper == null) {
+ throw new IllegalStateException("setupUpIndicator(Activity) has not been called");
+ }
+
+ mDrawerIndicatorEnabled = enabled;
+ if (enabled) {
+ mActionBarHelper.setActionBarUpIndicator(mSlideDrawable,
+ isMenuVisible() ? mDrawerOpenContentDesc : mDrawerClosedContentDesc);
+ } else {
+ mActionBarHelper.setActionBarUpIndicator(mThemeUpIndicator, 0);
+ }
+ }
+
+ /**
+ * Indicates whether the drawer indicator is currently enabled.
+ *
+ * @return Whether the drawer indicator is enabled.
+ */
+ public boolean isDrawerIndicatorEnabled() {
+ return mDrawerIndicatorEnabled;
+ }
+
+ /**
+ * 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 && mPosition != Position.BOTTOM) {
+ mMenuContainer.setPadding(0, insets.top, 0, 0);
+ }
+ return super.fitSystemWindows(insets);
+ }
+
+ protected void dispatchOnDrawerSlide(float openRatio, int offsetPixels) {
+ if (mOnDrawerStateChangeListener != null) {
+ mOnDrawerStateChangeListener.onDrawerSlide(openRatio, offsetPixels);
+ }
+ }
+
+ /**
+ * 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 CREATOR = new Creator() {
+ @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/main/java/net/simonvt/menudrawer/NoClickThroughFrameLayout.java b/menudrawer/src/main/java/net/simonvt/menudrawer/NoClickThroughFrameLayout.java
new file mode 100644
index 00000000..273a9166
--- /dev/null
+++ b/menudrawer/src/main/java/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.
+ */
+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/main/java/net/simonvt/menudrawer/OverlayDrawer.java b/menudrawer/src/main/java/net/simonvt/menudrawer/OverlayDrawer.java
new file mode 100644
index 00000000..fb463438
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/OverlayDrawer.java
@@ -0,0 +1,781 @@
+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;
+import android.view.VelocityTracker;
+import android.view.View;
+
+public class OverlayDrawer extends DraggableDrawer {
+
+ private static final String TAG = "OverlayDrawer";
+
+ private int mPeekSize;
+
+ private Runnable mRevealRunnable = new Runnable() {
+ @Override
+ public void run() {
+ cancelContentTouch();
+ int animateTo = 0;
+ switch (getPosition()) {
+ case RIGHT:
+ case BOTTOM:
+ animateTo = -mPeekSize;
+ break;
+
+ default:
+ animateTo = mPeekSize;
+ break;
+ }
+ animateOffsetTo(animateTo, 250);
+ }
+ };
+
+ OverlayDrawer(Activity activity, int dragMode) {
+ super(activity, dragMode);
+ }
+
+ public OverlayDrawer(Context context) {
+ super(context);
+ }
+
+ public OverlayDrawer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public OverlayDrawer(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);
+ super.addView(mContentContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ if (USE_TRANSLATIONS) {
+ mContentContainer.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ mContentContainer.setHardwareLayersEnabled(false);
+ super.addView(mMenuContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ mPeekSize = dpToPx(20);
+ }
+
+ @Override
+ protected void drawOverlay(Canvas canvas) {
+ final int width = getWidth();
+ final int height = getHeight();
+ final int offsetPixels = (int) mOffsetPixels;
+ final float openRatio = Math.abs(mOffsetPixels) / mMenuSize;
+
+ switch (getPosition()) {
+ case LEFT:
+ mMenuOverlay.setBounds(offsetPixels, 0, width, height);
+ break;
+
+ case RIGHT:
+ mMenuOverlay.setBounds(0, 0, width + offsetPixels, height);
+ break;
+
+ case TOP:
+ mMenuOverlay.setBounds(0, offsetPixels, width, height);
+ break;
+
+ case BOTTOM:
+ mMenuOverlay.setBounds(0, 0, width, height + offsetPixels);
+ break;
+ }
+
+ mMenuOverlay.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * openRatio));
+ mMenuOverlay.draw(canvas);
+ }
+
+ @Override
+ public void openMenu(boolean animate) {
+ int animateTo = 0;
+ switch (getPosition()) {
+ case LEFT:
+ case TOP:
+ animateTo = mMenuSize;
+ break;
+
+ case RIGHT:
+ case BOTTOM:
+ animateTo = -mMenuSize;
+ break;
+ }
+
+ animateOffsetTo(animateTo, 0, animate);
+ }
+
+ @Override
+ public void closeMenu(boolean animate) {
+ animateOffsetTo(0, 0, animate);
+ }
+
+ @Override
+ protected void onOffsetPixelsChanged(int offsetPixels) {
+ if (USE_TRANSLATIONS) {
+ switch (getPosition()) {
+ case LEFT:
+ mMenuContainer.setTranslationX(offsetPixels - mMenuSize);
+ break;
+
+ case TOP:
+ mMenuContainer.setTranslationY(offsetPixels - mMenuSize);
+ break;
+
+ case RIGHT:
+ mMenuContainer.setTranslationX(offsetPixels + mMenuSize);
+ break;
+
+ case BOTTOM:
+ mMenuContainer.setTranslationY(offsetPixels + mMenuSize);
+ break;
+ }
+ } else {
+ switch (getPosition()) {
+ case TOP:
+ mMenuContainer.offsetTopAndBottom(offsetPixels - mMenuContainer.getBottom());
+ break;
+
+ case BOTTOM:
+ mMenuContainer.offsetTopAndBottom(offsetPixels - (mMenuContainer.getTop() - getHeight()));
+ break;
+
+ case LEFT:
+ mMenuContainer.offsetLeftAndRight(offsetPixels - mMenuContainer.getRight());
+ break;
+
+ case RIGHT:
+ mMenuContainer.offsetLeftAndRight(offsetPixels - (mMenuContainer.getLeft() - getWidth()));
+ break;
+ }
+ }
+
+ invalidate();
+ }
+
+ @Override
+ protected void initPeekScroller() {
+ switch (getPosition()) {
+ case RIGHT:
+ case BOTTOM: {
+ final int dx = -mPeekSize;
+ mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
+ break;
+ }
+
+ default: {
+ final int dx = mPeekSize;
+ mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ onOffsetPixelsChanged((int) mOffsetPixels);
+ }
+
+ @Override
+ protected GradientDrawable.Orientation getDropShadowOrientation() {
+ switch (getPosition()) {
+ case TOP:
+ return GradientDrawable.Orientation.TOP_BOTTOM;
+
+ case RIGHT:
+ return GradientDrawable.Orientation.RIGHT_LEFT;
+
+ case BOTTOM:
+ return GradientDrawable.Orientation.BOTTOM_TOP;
+
+ default:
+ return GradientDrawable.Orientation.LEFT_RIGHT;
+ }
+ }
+
+ @Override
+ protected void updateDropShadowRect() {
+ final float openRatio = Math.abs(mOffsetPixels) / mMenuSize;
+ final int dropShadowSize = (int) (mDropShadowSize * openRatio);
+
+ switch (getPosition()) {
+ case LEFT:
+ mDropShadowRect.top = 0;
+ mDropShadowRect.bottom = getHeight();
+ mDropShadowRect.left = ViewHelper.getRight(mMenuContainer);
+ mDropShadowRect.right = mDropShadowRect.left + dropShadowSize;
+ break;
+
+ case TOP:
+ mDropShadowRect.left = 0;
+ mDropShadowRect.right = getWidth();
+ mDropShadowRect.top = ViewHelper.getBottom(mMenuContainer);
+ mDropShadowRect.bottom = mDropShadowRect.top + dropShadowSize;
+ break;
+
+ case RIGHT:
+ mDropShadowRect.top = 0;
+ mDropShadowRect.bottom = getHeight();
+ mDropShadowRect.right = ViewHelper.getLeft(mMenuContainer);
+ mDropShadowRect.left = mDropShadowRect.right - dropShadowSize;
+ break;
+
+ case BOTTOM:
+ mDropShadowRect.left = 0;
+ mDropShadowRect.right = getWidth();
+ mDropShadowRect.bottom = ViewHelper.getTop(mMenuContainer);
+ mDropShadowRect.top = mDropShadowRect.bottom - dropShadowSize;
+ break;
+ }
+ }
+
+ @Override
+ protected void startLayerTranslation() {
+ if (USE_TRANSLATIONS && mHardwareLayersEnabled && !mLayerTypeHardware) {
+ mLayerTypeHardware = true;
+ mMenuContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ }
+ }
+
+ @Override
+ protected void stopLayerTranslation() {
+ if (mLayerTypeHardware) {
+ mLayerTypeHardware = false;
+ mMenuContainer.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int width = r - l;
+ final int height = b - t;
+
+ mContentContainer.layout(0, 0, width, height);
+
+ if (USE_TRANSLATIONS) {
+ switch (getPosition()) {
+ case LEFT:
+ mMenuContainer.layout(0, 0, mMenuSize, height);
+ break;
+
+ case RIGHT:
+ mMenuContainer.layout(width - mMenuSize, 0, width, height);
+ break;
+
+ case TOP:
+ mMenuContainer.layout(0, 0, width, mMenuSize);
+ break;
+
+ case BOTTOM:
+ mMenuContainer.layout(0, height - mMenuSize, width, height);
+ break;
+ }
+
+ } else {
+ final int offsetPixels = (int) mOffsetPixels;
+ final int menuSize = mMenuSize;
+
+ switch (getPosition()) {
+ case LEFT:
+ mMenuContainer.layout(-menuSize + offsetPixels, 0, offsetPixels, height);
+ break;
+
+ case RIGHT:
+ mMenuContainer.layout(width + offsetPixels, 0, width + menuSize + offsetPixels, height);
+ break;
+
+ case TOP:
+ mMenuContainer.layout(0, -menuSize + offsetPixels, width, offsetPixels);
+ break;
+
+ case BOTTOM:
+ mMenuContainer.layout(0, height + offsetPixels, width, height + menuSize + offsetPixels);
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
+ throw new IllegalStateException("Must measure with an exact size");
+ }
+
+ final int width = MeasureSpec.getSize(widthMeasureSpec);
+ final int height = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (mOffsetPixels == -1) openMenu(false);
+
+ int menuWidthMeasureSpec;
+ int menuHeightMeasureSpec;
+ switch (getPosition()) {
+ case TOP:
+ case BOTTOM:
+ menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);
+ menuHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, mMenuSize);
+ break;
+
+ default:
+ // LEFT/RIGHT
+ menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, mMenuSize);
+ 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();
+ }
+
+ private boolean isContentTouch(int x, int y) {
+ boolean contentTouch = false;
+
+ switch (getPosition()) {
+ case LEFT:
+ contentTouch = ViewHelper.getRight(mMenuContainer) < x;
+ break;
+
+ case RIGHT:
+ contentTouch = ViewHelper.getLeft(mMenuContainer) > x;
+ break;
+
+ case TOP:
+ contentTouch = ViewHelper.getBottom(mMenuContainer) < y;
+ break;
+
+ case BOTTOM:
+ contentTouch = ViewHelper.getTop(mMenuContainer) > y;
+ break;
+ }
+
+ return contentTouch;
+ }
+
+ protected boolean onDownAllowDrag(int x, int y) {
+ switch (getPosition()) {
+ case LEFT:
+ return (!mMenuVisible && mInitialMotionX <= mTouchSize)
+ || (mMenuVisible && mInitialMotionX <= mOffsetPixels);
+
+ case RIGHT:
+ final int width = getWidth();
+ final int initialMotionX = (int) mInitialMotionX;
+
+ return (!mMenuVisible && initialMotionX >= width - mTouchSize)
+ || (mMenuVisible && initialMotionX >= width + mOffsetPixels);
+
+ case TOP:
+ return (!mMenuVisible && mInitialMotionY <= mTouchSize)
+ || (mMenuVisible && mInitialMotionY <= mOffsetPixels);
+
+ case BOTTOM:
+ final int height = getHeight();
+ return (!mMenuVisible && mInitialMotionY >= height - mTouchSize)
+ || (mMenuVisible && mInitialMotionY >= height + mOffsetPixels);
+ }
+
+ return false;
+ }
+
+ protected boolean onMoveAllowDrag(int x, int y, float dx, float dy) {
+ if (mMenuVisible && mTouchMode == TOUCH_MODE_FULLSCREEN) {
+ return true;
+ }
+
+ switch (getPosition()) {
+ case LEFT:
+ return (!mMenuVisible && mInitialMotionX <= mTouchSize && (dx > 0)) // Drawer closed
+ || (mMenuVisible && x <= mOffsetPixels) // Drawer open
+ || (Math.abs(mOffsetPixels) <= mPeekSize && mMenuVisible); // Drawer revealed
+
+ case RIGHT:
+ final int width = getWidth();
+ return (!mMenuVisible && mInitialMotionX >= width - mTouchSize && (dx < 0))
+ || (mMenuVisible && x >= width - mOffsetPixels)
+ || (Math.abs(mOffsetPixels) <= mPeekSize && mMenuVisible);
+
+ case TOP:
+ return (!mMenuVisible && mInitialMotionY <= mTouchSize && (dy > 0))
+ || (mMenuVisible && x <= mOffsetPixels)
+ || (Math.abs(mOffsetPixels) <= mPeekSize && mMenuVisible);
+
+ case BOTTOM:
+ final int height = getHeight();
+ return (!mMenuVisible && mInitialMotionY >= height - mTouchSize && (dy < 0))
+ || (mMenuVisible && x >= height - mOffsetPixels)
+ || (Math.abs(mOffsetPixels) <= mPeekSize && mMenuVisible);
+ }
+
+ return false;
+ }
+
+ protected void onMoveEvent(float dx, float dy) {
+ switch (getPosition()) {
+ case LEFT:
+ setOffsetPixels(Math.min(Math.max(mOffsetPixels + dx, 0), mMenuSize));
+ break;
+
+ case RIGHT:
+ setOffsetPixels(Math.max(Math.min(mOffsetPixels + dx, 0), -mMenuSize));
+ break;
+
+ case TOP:
+ setOffsetPixels(Math.min(Math.max(mOffsetPixels + dy, 0), mMenuSize));
+ break;
+
+ case BOTTOM:
+ setOffsetPixels(Math.max(Math.min(mOffsetPixels + dy, 0), -mMenuSize));
+ break;
+ }
+ }
+
+ protected void onUpEvent(int x, int y) {
+ final int offsetPixels = (int) mOffsetPixels;
+
+ switch (getPosition()) {
+ case LEFT: {
+ if (mIsDragging) {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+ final int initialVelocity = (int) getXVelocity(mVelocityTracker);
+ mLastMotionX = x;
+ animateOffsetTo(initialVelocity > 0 ? mMenuSize : 0, initialVelocity, true);
+
+ // Close the menu when content is clicked while the menu is visible.
+ } else if (mMenuVisible) {
+ closeMenu();
+ }
+ break;
+ }
+
+ case TOP: {
+ if (mIsDragging) {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+ final int initialVelocity = (int) getYVelocity(mVelocityTracker);
+ mLastMotionY = y;
+ animateOffsetTo(initialVelocity > 0 ? mMenuSize : 0, initialVelocity, true);
+
+ // Close the menu when content is clicked while the menu is visible.
+ } else if (mMenuVisible) {
+ closeMenu();
+ }
+ break;
+ }
+
+ case RIGHT: {
+ final int width = getWidth();
+
+ if (mIsDragging) {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+ final int initialVelocity = (int) getXVelocity(mVelocityTracker);
+ mLastMotionX = x;
+ animateOffsetTo(initialVelocity > 0 ? 0 : -mMenuSize, initialVelocity, true);
+
+ // Close the menu when content is clicked while the menu is visible.
+ } else if (mMenuVisible) {
+ closeMenu();
+ }
+ break;
+ }
+
+ case BOTTOM: {
+ if (mIsDragging) {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+ final int initialVelocity = (int) getYVelocity(mVelocityTracker);
+ mLastMotionY = y;
+ animateOffsetTo(initialVelocity < 0 ? -mMenuSize : 0, initialVelocity, true);
+
+ // Close the menu when content is clicked while the menu is visible.
+ } else if (mMenuVisible) {
+ closeMenu();
+ }
+ break;
+ }
+ }
+ }
+
+ protected boolean checkTouchSlop(float dx, float dy) {
+ switch (getPosition()) {
+ case TOP:
+ case BOTTOM:
+ return Math.abs(dy) > mTouchSlop && Math.abs(dy) > Math.abs(dx);
+
+ default:
+ return Math.abs(dx) > mTouchSlop && Math.abs(dx) > Math.abs(dy);
+ }
+ }
+
+ @Override
+ protected void stopAnimation() {
+ super.stopAnimation();
+ removeCallbacks(mRevealRunnable);
+ }
+
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ removeCallbacks(mRevealRunnable);
+ mActivePointerId = INVALID_POINTER;
+ mIsDragging = false;
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+
+ if (Math.abs(mOffsetPixels) > mMenuSize / 2) {
+ openMenu();
+ } else {
+ closeMenu();
+ }
+
+ return false;
+ }
+
+ if (action == MotionEvent.ACTION_DOWN && mMenuVisible && isCloseEnough()) {
+ setOffsetPixels(0);
+ stopAnimation();
+ endPeek();
+ setDrawerState(STATE_CLOSED);
+ mIsDragging = false;
+ }
+
+ // Always intercept events over the content while menu is visible.
+ if (mMenuVisible) {
+ int index = 0;
+ if (mActivePointerId != INVALID_POINTER) {
+ index = ev.findPointerIndex(mActivePointerId);
+ index = index == -1 ? 0 : index;
+ }
+
+ final int x = (int) ev.getX(index);
+ final int y = (int) ev.getY(index);
+ if (isContentTouch(x, y)) {
+ return true;
+ }
+ }
+
+ if (!mMenuVisible && !mIsDragging && mTouchMode == TOUCH_MODE_NONE) {
+ return false;
+ }
+
+ if (action != MotionEvent.ACTION_DOWN && mIsDragging) {
+ return true;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ mLastMotionX = mInitialMotionX = ev.getX();
+ mLastMotionY = mInitialMotionY = ev.getY();
+ final boolean allowDrag = onDownAllowDrag((int) mLastMotionX, (int) mLastMotionY);
+ mActivePointerId = ev.getPointerId(0);
+
+ if (allowDrag) {
+ setDrawerState(mMenuVisible ? STATE_OPEN : STATE_CLOSED);
+ stopAnimation();
+ endPeek();
+
+ if (!mMenuVisible && mInitialMotionX <= mPeekSize) {
+ postDelayed(mRevealRunnable, 160);
+ }
+
+ mIsDragging = false;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ final int activePointerId = mActivePointerId;
+ if (activePointerId == INVALID_POINTER) {
+ // If we don't have a valid id, the touch down wasn't on content.
+ break;
+ }
+
+ final int pointerIndex = ev.findPointerIndex(activePointerId);
+ if (pointerIndex == -1) {
+ mIsDragging = false;
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ closeMenu(true);
+ return false;
+ }
+
+ final float x = ev.getX(pointerIndex);
+ final float dx = x - mLastMotionX;
+ final float y = ev.getY(pointerIndex);
+ final float dy = y - mLastMotionY;
+
+ if (Math.abs(dx) >= mTouchSlop || Math.abs(dy) >= mTouchSlop) {
+ removeCallbacks(mRevealRunnable);
+ endPeek();
+ }
+
+ if (checkTouchSlop(dx, dy)) {
+ if (mOnInterceptMoveEventListener != null && (mTouchMode == TOUCH_MODE_FULLSCREEN || mMenuVisible)
+ && canChildrenScroll((int) dx, (int) dy, (int) x, (int) y)) {
+ endDrag(); // Release the velocity tracker
+ requestDisallowInterceptTouchEvent(true);
+ return false;
+ }
+
+ final boolean allowDrag = onMoveAllowDrag((int) x, (int) y, dx, dy);
+
+ if (allowDrag) {
+ endPeek();
+ stopAnimation();
+ setDrawerState(STATE_DRAGGING);
+ mIsDragging = true;
+ mLastMotionX = x;
+ mLastMotionY = y;
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_UP:
+ onPointerUp(ev);
+ mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
+ mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
+ 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((int) mLastMotionX, (int) mLastMotionY);
+
+ mActivePointerId = ev.getPointerId(0);
+
+ if (allowDrag) {
+ stopAnimation();
+ endPeek();
+
+ if (!mMenuVisible && mLastMotionX <= mPeekSize) {
+ postDelayed(mRevealRunnable, 160);
+ }
+
+ startLayerTranslation();
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ final int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ mIsDragging = false;
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ closeMenu(true);
+ return false;
+ }
+
+ if (!mIsDragging) {
+ final float x = ev.getX(pointerIndex);
+ final float dx = x - mLastMotionX;
+ final float y = ev.getY(pointerIndex);
+ final float dy = y - mLastMotionY;
+
+ if (checkTouchSlop(dx, dy)) {
+ final boolean allowDrag = onMoveAllowDrag((int) x, (int) y, dx, dy);
+
+ if (allowDrag) {
+ endPeek();
+ stopAnimation();
+ setDrawerState(STATE_DRAGGING);
+ mIsDragging = true;
+ mLastMotionX = x;
+ mLastMotionY = y;
+ } else {
+ mInitialMotionX = x;
+ mInitialMotionY = y;
+ }
+ }
+ }
+
+ if (mIsDragging) {
+ startLayerTranslation();
+
+ final float x = ev.getX(pointerIndex);
+ final float dx = x - mLastMotionX;
+ final float y = ev.getY(pointerIndex);
+ final float dy = y - mLastMotionY;
+
+ mLastMotionX = x;
+ mLastMotionY = y;
+ onMoveEvent(dx, dy);
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ removeCallbacks(mRevealRunnable);
+ int index = ev.findPointerIndex(mActivePointerId);
+ index = index == -1 ? 0 : index;
+ final int x = (int) ev.getX(index);
+ final int y = (int) ev.getY(index);
+ onUpEvent(x, y);
+ mActivePointerId = INVALID_POINTER;
+ mIsDragging = false;
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_DOWN:
+ final int index = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ mLastMotionX = ev.getX(index);
+ mLastMotionY = ev.getY(index);
+ mActivePointerId = ev.getPointerId(index);
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ onPointerUp(ev);
+ mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
+ mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
+ break;
+ }
+
+ return true;
+ }
+
+ private void onPointerUp(MotionEvent ev) {
+ final int pointerIndex = ev.getActionIndex();
+ final int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mLastMotionX = ev.getX(newPointerIndex);
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ }
+ }
+}
diff --git a/menudrawer/src/main/java/net/simonvt/menudrawer/PeekInterpolator.java b/menudrawer/src/main/java/net/simonvt/menudrawer/PeekInterpolator.java
new file mode 100644
index 00000000..528fd845
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/PeekInterpolator.java
@@ -0,0 +1,28 @@
+package net.simonvt.menudrawer;
+
+import android.view.animation.Interpolator;
+
+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/main/java/net/simonvt/menudrawer/Position.java b/menudrawer/src/main/java/net/simonvt/menudrawer/Position.java
new file mode 100644
index 00000000..a2185b2f
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/Position.java
@@ -0,0 +1,50 @@
+package net.simonvt.menudrawer;
+
+import android.util.SparseArray;
+
+/**
+ * Enums used for positioning the drawer.
+ */
+public enum Position {
+ // Positions the drawer to the left of the content.
+ LEFT(0),
+
+ // Positions the drawer above the content.
+ TOP(1),
+
+ // Positions the drawer to the right of the content.
+ RIGHT(2),
+
+ // Positions the drawer below the content.
+ BOTTOM(3),
+
+ /**
+ * Position the drawer at the start edge. This will position the drawer to the {@link #LEFT} with LTR languages and
+ * {@link #RIGHT} with RTL languages.
+ */
+ START(4),
+
+ /**
+ * Position the drawer at the end edge. This will position the drawer to the {@link #RIGHT} with LTR languages and
+ * {@link #LEFT} with RTL languages.
+ */
+ END(5);
+
+ final int mValue;
+
+ Position(int value) {
+ mValue = value;
+ }
+
+ private static final SparseArray STRING_MAPPING = new SparseArray();
+
+ static {
+ for (Position via : Position.values()) {
+ STRING_MAPPING.put(via.mValue, via);
+ }
+ }
+
+ public static Position fromValue(int value) {
+ return STRING_MAPPING.get(value);
+ }
+}
diff --git a/menudrawer/src/main/java/net/simonvt/menudrawer/Scroller.java b/menudrawer/src/main/java/net/simonvt/menudrawer/Scroller.java
new file mode 100644
index 00000000..0b7464fd
--- /dev/null
+++ b/menudrawer/src/main/java/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.
+ */
+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/main/java/net/simonvt/menudrawer/SinusoidalInterpolator.java b/menudrawer/src/main/java/net/simonvt/menudrawer/SinusoidalInterpolator.java
new file mode 100644
index 00000000..44199696
--- /dev/null
+++ b/menudrawer/src/main/java/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.
+ */
+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/main/java/net/simonvt/menudrawer/SlideDrawable.java b/menudrawer/src/main/java/net/simonvt/menudrawer/SlideDrawable.java
new file mode 100644
index 00000000..4020f5da
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/SlideDrawable.java
@@ -0,0 +1,187 @@
+package net.simonvt.menudrawer;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.Drawable;
+
+public class SlideDrawable extends Drawable implements Drawable.Callback {
+
+ private Drawable mWrapped;
+ private float mOffset;
+
+ private final Rect mTmpRect = new Rect();
+
+ private boolean mIsRtl;
+
+ public SlideDrawable(Drawable wrapped) {
+ mWrapped = wrapped;
+ }
+
+ public void setOffset(float offset) {
+ mOffset = offset;
+ invalidateSelf();
+ }
+
+ public float getOffset() {
+ return mOffset;
+ }
+
+ void setIsRtl(boolean isRtl) {
+ mIsRtl = isRtl;
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ mWrapped.copyBounds(mTmpRect);
+ canvas.save();
+ if (mIsRtl) {
+ canvas.translate(1.f / 3 * mTmpRect.width() * mOffset, 0);
+ } else {
+ canvas.translate(1.f / 3 * mTmpRect.width() * -mOffset, 0);
+ }
+ mWrapped.draw(canvas);
+ canvas.restore();
+ }
+
+ @Override
+ public void setChangingConfigurations(int configs) {
+ mWrapped.setChangingConfigurations(configs);
+ }
+
+ @Override
+ public int getChangingConfigurations() {
+ return mWrapped.getChangingConfigurations();
+ }
+
+ @Override
+ public void setDither(boolean dither) {
+ mWrapped.setDither(dither);
+ }
+
+ @Override
+ public void setFilterBitmap(boolean filter) {
+ mWrapped.setFilterBitmap(filter);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mWrapped.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ mWrapped.setColorFilter(cf);
+ }
+
+ @Override
+ public void setColorFilter(int color, PorterDuff.Mode mode) {
+ mWrapped.setColorFilter(color, mode);
+ }
+
+ @Override
+ public void clearColorFilter() {
+ mWrapped.clearColorFilter();
+ }
+
+ @Override
+ public boolean isStateful() {
+ return mWrapped.isStateful();
+ }
+
+ @Override
+ public boolean setState(int[] stateSet) {
+ return mWrapped.setState(stateSet);
+ }
+
+ @Override
+ public int[] getState() {
+ return mWrapped.getState();
+ }
+
+ @Override
+ public Drawable getCurrent() {
+ return mWrapped.getCurrent();
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ return super.setVisible(visible, restart);
+ }
+
+ @Override
+ public int getOpacity() {
+ return mWrapped.getOpacity();
+ }
+
+ @Override
+ public Region getTransparentRegion() {
+ return mWrapped.getTransparentRegion();
+ }
+
+ @Override
+ protected boolean onStateChange(int[] state) {
+ mWrapped.setState(state);
+ return super.onStateChange(state);
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ mWrapped.setBounds(bounds);
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mWrapped.getIntrinsicWidth();
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mWrapped.getIntrinsicHeight();
+ }
+
+ @Override
+ public int getMinimumWidth() {
+ return mWrapped.getMinimumWidth();
+ }
+
+ @Override
+ public int getMinimumHeight() {
+ return mWrapped.getMinimumHeight();
+ }
+
+ @Override
+ public boolean getPadding(Rect padding) {
+ return mWrapped.getPadding(padding);
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ return super.getConstantState();
+ }
+
+ @Override
+ public void invalidateDrawable(Drawable who) {
+ if (who == mWrapped) {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void scheduleDrawable(Drawable who, Runnable what, long when) {
+ if (who == mWrapped) {
+ scheduleSelf(what, when);
+ }
+ }
+
+ @Override
+ public void unscheduleDrawable(Drawable who, Runnable what) {
+ if (who == mWrapped) {
+ unscheduleSelf(what);
+ }
+ }
+}
diff --git a/menudrawer/src/main/java/net/simonvt/menudrawer/SlidingDrawer.java b/menudrawer/src/main/java/net/simonvt/menudrawer/SlidingDrawer.java
new file mode 100644
index 00000000..1d2530da
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/SlidingDrawer.java
@@ -0,0 +1,707 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+
+public class SlidingDrawer extends DraggableDrawer {
+
+ private static final String TAG = "OverlayDrawer";
+
+ SlidingDrawer(Activity activity, int dragMode) {
+ super(activity, dragMode);
+ }
+
+ public SlidingDrawer(Context context) {
+ super(context);
+ }
+
+ public SlidingDrawer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SlidingDrawer(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);
+ super.addView(mMenuContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ super.addView(mContentContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ }
+
+ @Override
+ public void openMenu(boolean animate) {
+ int animateTo = 0;
+ switch (getPosition()) {
+ case LEFT:
+ case TOP:
+ animateTo = mMenuSize;
+ break;
+
+ case RIGHT:
+ case BOTTOM:
+ animateTo = -mMenuSize;
+ break;
+ }
+
+ animateOffsetTo(animateTo, 0, animate);
+ }
+
+ @Override
+ public void closeMenu(boolean animate) {
+ animateOffsetTo(0, 0, animate);
+ }
+
+ @Override
+ protected void onOffsetPixelsChanged(int offsetPixels) {
+ if (USE_TRANSLATIONS) {
+ switch (getPosition()) {
+ case TOP:
+ case BOTTOM:
+ mContentContainer.setTranslationY(offsetPixels);
+ break;
+
+ default:
+ mContentContainer.setTranslationX(offsetPixels);
+ break;
+ }
+ } else {
+ switch (getPosition()) {
+ case TOP:
+ case BOTTOM:
+ mContentContainer.offsetTopAndBottom(offsetPixels - mContentContainer.getTop());
+ break;
+
+ default:
+ mContentContainer.offsetLeftAndRight(offsetPixels - mContentContainer.getLeft());
+ break;
+ }
+ }
+
+ offsetMenu(offsetPixels);
+ invalidate();
+ }
+
+ @Override
+ protected void initPeekScroller() {
+ switch (getPosition()) {
+ case RIGHT:
+ case BOTTOM: {
+ final int dx = -mMenuSize / 3;
+ mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
+ break;
+ }
+
+ default: {
+ final int dx = mMenuSize / 3;
+ mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ onOffsetPixelsChanged((int) mOffsetPixels);
+ }
+
+ @Override
+ protected void drawOverlay(Canvas canvas) {
+ final int width = getWidth();
+ final int height = getHeight();
+ final int offsetPixels = (int) mOffsetPixels;
+ final float openRatio = Math.abs(mOffsetPixels) / mMenuSize;
+
+ switch (getPosition()) {
+ case LEFT:
+ mMenuOverlay.setBounds(0, 0, offsetPixels, height);
+ break;
+
+ case RIGHT:
+ mMenuOverlay.setBounds(width + offsetPixels, 0, width, height);
+ break;
+
+ case TOP:
+ mMenuOverlay.setBounds(0, 0, width, offsetPixels);
+ break;
+
+ case BOTTOM:
+ mMenuOverlay.setBounds(0, height + offsetPixels, width, height);
+ break;
+ }
+
+ mMenuOverlay.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * (1.f - openRatio)));
+ mMenuOverlay.draw(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;
+
+ if (USE_TRANSLATIONS) {
+ mContentContainer.layout(0, 0, width, height);
+ } else {
+ final int offsetPixels = (int) mOffsetPixels;
+ if (getPosition() == Position.LEFT || getPosition() == Position.RIGHT) {
+ mContentContainer.layout(offsetPixels, 0, width + offsetPixels, height);
+ } else {
+ mContentContainer.layout(0, offsetPixels, width, height + offsetPixels);
+ }
+ }
+
+ switch (getPosition()) {
+ case LEFT:
+ mMenuContainer.layout(0, 0, mMenuSize, height);
+ break;
+
+ case RIGHT:
+ mMenuContainer.layout(width - mMenuSize, 0, width, height);
+ break;
+
+ case TOP:
+ mMenuContainer.layout(0, 0, width, mMenuSize);
+ break;
+
+ case BOTTOM:
+ mMenuContainer.layout(0, height - mMenuSize, width, height);
+ break;
+ }
+ }
+
+ /**
+ * 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) {
+ return;
+ }
+
+ final int width = getWidth();
+ final int height = getHeight();
+
+ final int menuSize = mMenuSize;
+ final int sign = (int) (mOffsetPixels / Math.abs(mOffsetPixels));
+ final float openRatio = Math.abs(mOffsetPixels) / menuSize;
+ final int offset = (int) (-0.25f * ((1.0f - openRatio) * menuSize) * sign);
+
+ switch (getPosition()) {
+ case LEFT: {
+ if (USE_TRANSLATIONS) {
+ if (offsetPixels > 0) {
+ mMenuContainer.setTranslationX(offset);
+ } else {
+ mMenuContainer.setTranslationX(-menuSize);
+ }
+
+ } else {
+ mMenuContainer.offsetLeftAndRight(offset - mMenuContainer.getLeft());
+ mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
+ }
+ break;
+ }
+
+ case RIGHT: {
+ if (USE_TRANSLATIONS) {
+ if (offsetPixels != 0) {
+ mMenuContainer.setTranslationX(offset);
+ } else {
+ mMenuContainer.setTranslationX(menuSize);
+ }
+
+ } else {
+ final int oldOffset = mMenuContainer.getRight() - width;
+ final int offsetBy = offset - oldOffset;
+ mMenuContainer.offsetLeftAndRight(offsetBy);
+ mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
+ }
+ break;
+ }
+
+ case TOP: {
+ if (USE_TRANSLATIONS) {
+ if (offsetPixels > 0) {
+ mMenuContainer.setTranslationY(offset);
+ } else {
+ mMenuContainer.setTranslationY(-menuSize);
+ }
+
+ } else {
+ mMenuContainer.offsetTopAndBottom(offset - mMenuContainer.getTop());
+ mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
+ }
+ break;
+ }
+
+ case BOTTOM: {
+ if (USE_TRANSLATIONS) {
+ if (offsetPixels != 0) {
+ mMenuContainer.setTranslationY(offset);
+ } else {
+ mMenuContainer.setTranslationY(menuSize);
+ }
+
+ } else {
+ final int oldOffset = mMenuContainer.getBottom() - height;
+ final int offsetBy = offset - oldOffset;
+ mMenuContainer.offsetTopAndBottom(offsetBy);
+ mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
+ }
+ break;
+ }
+ }
+
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
+ throw new IllegalStateException("Must measure with an exact size");
+ }
+
+ final int width = MeasureSpec.getSize(widthMeasureSpec);
+ final int height = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (mOffsetPixels == -1) openMenu(false);
+
+ int menuWidthMeasureSpec;
+ int menuHeightMeasureSpec;
+ switch (getPosition()) {
+ case TOP:
+ case BOTTOM:
+ menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);
+ menuHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, mMenuSize);
+ break;
+
+ default:
+ // LEFT/RIGHT
+ menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, mMenuSize);
+ 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();
+ }
+
+ private boolean isContentTouch(int x, int y) {
+ boolean contentTouch = false;
+
+ switch (getPosition()) {
+ case LEFT:
+ contentTouch = ViewHelper.getLeft(mContentContainer) < x;
+ break;
+
+ case RIGHT:
+ contentTouch = ViewHelper.getRight(mContentContainer) > x;
+ break;
+
+ case TOP:
+ contentTouch = ViewHelper.getTop(mContentContainer) < y;
+ break;
+
+ case BOTTOM:
+ contentTouch = ViewHelper.getBottom(mContentContainer) > y;
+ break;
+ }
+
+ return contentTouch;
+ }
+
+ protected boolean onDownAllowDrag(int x, int y) {
+ switch (getPosition()) {
+ case LEFT:
+ return (!mMenuVisible && mInitialMotionX <= mTouchSize)
+ || (mMenuVisible && mInitialMotionX >= mOffsetPixels);
+
+ case RIGHT:
+ final int width = getWidth();
+ final int initialMotionX = (int) mInitialMotionX;
+
+ return (!mMenuVisible && initialMotionX >= width - mTouchSize)
+ || (mMenuVisible && initialMotionX <= width + mOffsetPixels);
+
+ case TOP:
+ return (!mMenuVisible && mInitialMotionY <= mTouchSize)
+ || (mMenuVisible && mInitialMotionY >= mOffsetPixels);
+
+ case BOTTOM:
+ final int height = getHeight();
+ return (!mMenuVisible && mInitialMotionY >= height - mTouchSize)
+ || (mMenuVisible && mInitialMotionY <= height + mOffsetPixels);
+ }
+
+ return false;
+ }
+
+ protected boolean onMoveAllowDrag(int x, int y, float dx, float dy) {
+ switch (getPosition()) {
+ case LEFT:
+ return (!mMenuVisible && mInitialMotionX <= mTouchSize && (dx > 0))
+ || (mMenuVisible && x >= mOffsetPixels);
+
+ case RIGHT:
+ final int width = getWidth();
+ return (!mMenuVisible && mInitialMotionX >= width - mTouchSize && (dx < 0))
+ || (mMenuVisible && x <= width + mOffsetPixels);
+
+ case TOP:
+ return (!mMenuVisible && mInitialMotionY <= mTouchSize && (dy > 0))
+ || (mMenuVisible && y >= mOffsetPixels);
+
+ case BOTTOM:
+ final int height = getHeight();
+ return (!mMenuVisible && mInitialMotionY >= height - mTouchSize && (dy < 0))
+ || (mMenuVisible && y <= height + mOffsetPixels);
+ }
+
+ return false;
+ }
+
+ protected void onMoveEvent(float dx, float dy) {
+ switch (getPosition()) {
+ case LEFT:
+ setOffsetPixels(Math.min(Math.max(mOffsetPixels + dx, 0), mMenuSize));
+ break;
+
+ case RIGHT:
+ setOffsetPixels(Math.max(Math.min(mOffsetPixels + dx, 0), -mMenuSize));
+ break;
+
+ case TOP:
+ setOffsetPixels(Math.min(Math.max(mOffsetPixels + dy, 0), mMenuSize));
+ break;
+
+ case BOTTOM:
+ setOffsetPixels(Math.max(Math.min(mOffsetPixels + dy, 0), -mMenuSize));
+ break;
+ }
+ }
+
+ protected void onUpEvent(int x, int y) {
+ final int offsetPixels = (int) mOffsetPixels;
+
+ switch (getPosition()) {
+ case LEFT: {
+ if (mIsDragging) {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+ final int initialVelocity = (int) getXVelocity(mVelocityTracker);
+ mLastMotionX = x;
+ animateOffsetTo(initialVelocity > 0 ? mMenuSize : 0, initialVelocity, true);
+
+ // Close the menu when content is clicked while the menu is visible.
+ } else if (mMenuVisible && x > offsetPixels) {
+ closeMenu();
+ }
+ break;
+ }
+
+ case TOP: {
+ if (mIsDragging) {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+ final int initialVelocity = (int) getYVelocity(mVelocityTracker);
+ mLastMotionY = y;
+ animateOffsetTo(initialVelocity > 0 ? mMenuSize : 0, initialVelocity, true);
+
+ // Close the menu when content is clicked while the menu is visible.
+ } else if (mMenuVisible && y > offsetPixels) {
+ closeMenu();
+ }
+ break;
+ }
+
+ case RIGHT: {
+ final int width = getWidth();
+
+ if (mIsDragging) {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+ final int initialVelocity = (int) getXVelocity(mVelocityTracker);
+ mLastMotionX = x;
+ animateOffsetTo(initialVelocity > 0 ? 0 : -mMenuSize, initialVelocity, true);
+
+ // Close the menu when content is clicked while the menu is visible.
+ } else if (mMenuVisible && x < width + offsetPixels) {
+ closeMenu();
+ }
+ break;
+ }
+
+ case BOTTOM: {
+ if (mIsDragging) {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+ final int initialVelocity = (int) getYVelocity(mVelocityTracker);
+ mLastMotionY = y;
+ animateOffsetTo(initialVelocity < 0 ? -mMenuSize : 0, initialVelocity, true);
+
+ // Close the menu when content is clicked while the menu is visible.
+ } else if (mMenuVisible && y < getHeight() + offsetPixels) {
+ closeMenu();
+ }
+ break;
+ }
+ }
+ }
+
+ protected boolean checkTouchSlop(float dx, float dy) {
+ switch (getPosition()) {
+ case TOP:
+ case BOTTOM:
+ return Math.abs(dy) > mTouchSlop && Math.abs(dy) > Math.abs(dx);
+
+ default:
+ return Math.abs(dx) > mTouchSlop && Math.abs(dx) > Math.abs(dy);
+ }
+ }
+
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mActivePointerId = INVALID_POINTER;
+ mIsDragging = false;
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+
+ if (Math.abs(mOffsetPixels) > mMenuSize / 2) {
+ openMenu();
+ } else {
+ closeMenu();
+ }
+
+ return false;
+ }
+
+ if (action == MotionEvent.ACTION_DOWN && mMenuVisible && isCloseEnough()) {
+ setOffsetPixels(0);
+ stopAnimation();
+ endPeek();
+ setDrawerState(STATE_CLOSED);
+ mIsDragging = false;
+ }
+
+ // Always intercept events over the content while menu is visible.
+ if (mMenuVisible) {
+ int index = 0;
+ if (mActivePointerId != INVALID_POINTER) {
+ index = ev.findPointerIndex(mActivePointerId);
+ index = index == -1 ? 0 : index;
+ }
+
+ final int x = (int) ev.getX(index);
+ final int y = (int) ev.getY(index);
+ if (isContentTouch(x, y)) {
+ return true;
+ }
+ }
+
+ if (!mMenuVisible && !mIsDragging && mTouchMode == TOUCH_MODE_NONE) {
+ return false;
+ }
+
+ if (action != MotionEvent.ACTION_DOWN && mIsDragging) {
+ return true;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ mLastMotionX = mInitialMotionX = ev.getX();
+ mLastMotionY = mInitialMotionY = ev.getY();
+ final boolean allowDrag = onDownAllowDrag((int) mLastMotionX, (int) mLastMotionY);
+ mActivePointerId = ev.getPointerId(0);
+
+ if (allowDrag) {
+ setDrawerState(mMenuVisible ? STATE_OPEN : STATE_CLOSED);
+ stopAnimation();
+ endPeek();
+ mIsDragging = false;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ final int activePointerId = mActivePointerId;
+ if (activePointerId == INVALID_POINTER) {
+ // If we don't have a valid id, the touch down wasn't on content.
+ break;
+ }
+
+ final int pointerIndex = ev.findPointerIndex(activePointerId);
+ if (pointerIndex == -1) {
+ mIsDragging = false;
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ closeMenu(true);
+ return false;
+ }
+
+ final float x = ev.getX(pointerIndex);
+ final float dx = x - mLastMotionX;
+ final float y = ev.getY(pointerIndex);
+ final float dy = y - mLastMotionY;
+
+ if (checkTouchSlop(dx, dy)) {
+ if (mOnInterceptMoveEventListener != null && (mTouchMode == TOUCH_MODE_FULLSCREEN || mMenuVisible)
+ && canChildrenScroll((int) dx, (int) dy, (int) x, (int) y)) {
+ endDrag(); // Release the velocity tracker
+ requestDisallowInterceptTouchEvent(true);
+ return false;
+ }
+
+ final boolean allowDrag = onMoveAllowDrag((int) x, (int) y, dx, dy);
+
+ if (allowDrag) {
+ setDrawerState(STATE_DRAGGING);
+ mIsDragging = true;
+ mLastMotionX = x;
+ mLastMotionY = y;
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_UP:
+ onPointerUp(ev);
+ mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
+ mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
+ 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((int) mLastMotionX, (int) mLastMotionY);
+
+ mActivePointerId = ev.getPointerId(0);
+
+ if (allowDrag) {
+ stopAnimation();
+ endPeek();
+ startLayerTranslation();
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ final int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ mIsDragging = false;
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ closeMenu(true);
+ return false;
+ }
+
+ if (!mIsDragging) {
+ final float x = ev.getX(pointerIndex);
+ final float dx = x - mLastMotionX;
+ final float y = ev.getY(pointerIndex);
+ final float dy = y - mLastMotionY;
+
+ if (checkTouchSlop(dx, dy)) {
+ final boolean allowDrag = onMoveAllowDrag((int) x, (int) y, dx, dy);
+
+ if (allowDrag) {
+ setDrawerState(STATE_DRAGGING);
+ mIsDragging = true;
+ mLastMotionX = x;
+ mLastMotionY = y;
+ } else {
+ mInitialMotionX = x;
+ mInitialMotionY = y;
+ }
+ }
+ }
+
+ if (mIsDragging) {
+ startLayerTranslation();
+
+ final float x = ev.getX(pointerIndex);
+ final float dx = x - mLastMotionX;
+ final float y = ev.getY(pointerIndex);
+ final float dy = y - mLastMotionY;
+
+ mLastMotionX = x;
+ mLastMotionY = y;
+ onMoveEvent(dx, dy);
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ int index = ev.findPointerIndex(mActivePointerId);
+ index = index == -1 ? 0 : index;
+ final int x = (int) ev.getX(index);
+ final int y = (int) ev.getY(index);
+ onUpEvent(x, y);
+ mActivePointerId = INVALID_POINTER;
+ mIsDragging = false;
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_DOWN:
+ final int index = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ mLastMotionX = ev.getX(index);
+ mLastMotionY = ev.getY(index);
+ mActivePointerId = ev.getPointerId(index);
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ onPointerUp(ev);
+ mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
+ mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
+ break;
+ }
+
+ return true;
+ }
+
+ private void onPointerUp(MotionEvent ev) {
+ final int pointerIndex = ev.getActionIndex();
+ final int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mLastMotionX = ev.getX(newPointerIndex);
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ }
+ }
+}
diff --git a/menudrawer/src/main/java/net/simonvt/menudrawer/SmoothInterpolator.java b/menudrawer/src/main/java/net/simonvt/menudrawer/SmoothInterpolator.java
new file mode 100644
index 00000000..2f15d29f
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/SmoothInterpolator.java
@@ -0,0 +1,12 @@
+package net.simonvt.menudrawer;
+
+import android.view.animation.Interpolator;
+
+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/main/java/net/simonvt/menudrawer/StaticDrawer.java b/menudrawer/src/main/java/net/simonvt/menudrawer/StaticDrawer.java
new file mode 100644
index 00000000..bad120bb
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/StaticDrawer.java
@@ -0,0 +1,218 @@
+package net.simonvt.menudrawer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+
+public class StaticDrawer extends MenuDrawer {
+
+ 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 initDrawer(Context context, AttributeSet attrs, int defStyle) {
+ super.initDrawer(context, attrs, defStyle);
+ super.addView(mMenuContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ super.addView(mContentContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+
+ mIsStatic = true;
+ }
+
+ @Override
+ protected void drawOverlay(Canvas canvas) {
+ // NO-OP
+ }
+
+ @Override
+ protected void onOffsetPixelsChanged(int offsetPixels) {
+ // NO-OP
+ }
+
+ @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 (getPosition()) {
+ 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.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
+ throw new IllegalStateException("Must measure with an exact size");
+ }
+
+ final int width = MeasureSpec.getSize(widthMeasureSpec);
+ final int height = MeasureSpec.getSize(heightMeasureSpec);
+
+ switch (getPosition()) {
+ 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) {
+ // NO-OP
+ }
+
+ @Override
+ public void openMenu(boolean animate) {
+ // NO-OP
+ }
+
+ @Override
+ public void closeMenu(boolean animate) {
+ // NO-OP
+ }
+
+ @Override
+ public boolean isMenuVisible() {
+ return true;
+ }
+
+ @Override
+ public void setMenuSize(int size) {
+ mMenuSize = size;
+ requestLayout();
+ invalidate();
+ }
+
+ @Override
+ public void setOffsetMenuEnabled(boolean offsetMenu) {
+ // NO-OP
+ }
+
+ @Override
+ public boolean getOffsetMenuEnabled() {
+ return false;
+ }
+
+ @Override
+ public void peekDrawer() {
+ // NO-OP
+ }
+
+ @Override
+ public void peekDrawer(long delay) {
+ // NO-OP
+ }
+
+ @Override
+ public void peekDrawer(long startDelay, long delay) {
+ // NO-OP
+ }
+
+ @Override
+ public void setHardwareLayerEnabled(boolean enabled) {
+ // NO-OP
+ }
+
+ @Override
+ public int getTouchMode() {
+ return TOUCH_MODE_NONE;
+ }
+
+ @Override
+ public void setTouchMode(int mode) {
+ // NO-OP
+ }
+
+ @Override
+ public void setTouchBezelSize(int size) {
+ // NO-OP
+ }
+
+ @Override
+ public int getTouchBezelSize() {
+ return 0;
+ }
+
+ @Override
+ public void setSlideDrawable(int drawableRes) {
+ // NO-OP
+ }
+
+ @Override
+ public void setSlideDrawable(Drawable drawable) {
+ // NO-OP
+ }
+
+ @Override
+ public void setupUpIndicator(Activity activity) {
+ // NO-OP
+ }
+
+ @Override
+ public void setDrawerIndicatorEnabled(boolean enabled) {
+ // NO-OP
+ }
+
+ @Override
+ public boolean isDrawerIndicatorEnabled() {
+ return false;
+ }
+}
diff --git a/menudrawer/src/main/java/net/simonvt/menudrawer/ViewHelper.java b/menudrawer/src/main/java/net/simonvt/menudrawer/ViewHelper.java
new file mode 100644
index 00000000..beb0ebec
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/ViewHelper.java
@@ -0,0 +1,50 @@
+package net.simonvt.menudrawer;
+
+import android.os.Build;
+import android.view.View;
+
+final class ViewHelper {
+
+ private ViewHelper() {
+ }
+
+ public static int getLeft(View v) {
+ if (MenuDrawer.USE_TRANSLATIONS) {
+ return (int) (v.getLeft() + v.getTranslationX());
+ }
+
+ return v.getLeft();
+ }
+
+ public static int getTop(View v) {
+ if (MenuDrawer.USE_TRANSLATIONS) {
+ return (int) (v.getTop() + v.getTranslationY());
+ }
+
+ return v.getTop();
+ }
+
+ public static int getRight(View v) {
+ if (MenuDrawer.USE_TRANSLATIONS) {
+ return (int) (v.getRight() + v.getTranslationX());
+ }
+
+ return v.getRight();
+ }
+
+ public static int getBottom(View v) {
+ if (MenuDrawer.USE_TRANSLATIONS) {
+ return (int) (v.getBottom() + v.getTranslationY());
+ }
+
+ return v.getBottom();
+ }
+
+ public static int getLayoutDirection(View v) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return v.getLayoutDirection();
+ }
+
+ return View.LAYOUT_DIRECTION_LTR;
+ }
+}
diff --git a/menudrawer/src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java b/menudrawer/src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java
new file mode 100644
index 00000000..80c236d1
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java
@@ -0,0 +1,83 @@
+package net.simonvt.menudrawer.compat;
+
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.Log;
+
+import java.lang.reflect.Method;
+
+public final class ActionBarHelper {
+
+ private static final String TAG = "ActionBarHelper";
+
+ static final boolean DEBUG = false;
+
+ private Activity mActivity;
+
+ private Object mIndicatorInfo;
+
+ private boolean mUsesCompat;
+
+ public ActionBarHelper(Activity activity) {
+ mActivity = activity;
+
+ try {
+ Class clazz = activity.getClass();
+ Method m = clazz.getMethod("getSupportActionBar");
+ mUsesCompat = true;
+ } catch (NoSuchMethodException e) {
+ if (DEBUG) {
+ Log.e(TAG,
+ "Activity " + activity.getClass().getSimpleName() + " does not use a compatibility action bar",
+ e);
+ }
+ }
+
+ mIndicatorInfo = getIndicatorInfo();
+ }
+
+ private Object getIndicatorInfo() {
+ if (mUsesCompat && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ return ActionBarHelperCompat.getIndicatorInfo(mActivity);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ return ActionBarHelperNative.getIndicatorInfo(mActivity);
+ }
+
+ return null;
+ }
+
+ public void setActionBarUpIndicator(Drawable drawable, int contentDesc) {
+ if (mUsesCompat && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ ActionBarHelperCompat.setActionBarUpIndicator(mIndicatorInfo, mActivity, drawable, contentDesc);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ ActionBarHelperNative.setActionBarUpIndicator(mIndicatorInfo, mActivity, drawable, contentDesc);
+ }
+ }
+
+ public void setActionBarDescription(int contentDesc) {
+ if (mUsesCompat && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ ActionBarHelperCompat.setActionBarDescription(mIndicatorInfo, mActivity, contentDesc);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ ActionBarHelperNative.setActionBarDescription(mIndicatorInfo, mActivity, contentDesc);
+ }
+ }
+
+ public Drawable getThemeUpIndicator() {
+ if (mUsesCompat && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ return ActionBarHelperCompat.getThemeUpIndicator(mIndicatorInfo);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ return ActionBarHelperNative.getThemeUpIndicator(mIndicatorInfo, mActivity);
+ }
+
+ return null;
+ }
+
+ public void setDisplayShowHomeAsUpEnabled(boolean enabled) {
+ if (mUsesCompat && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ ActionBarHelperCompat.setDisplayHomeAsUpEnabled(mIndicatorInfo, enabled);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ ActionBarHelperNative.setDisplayHomeAsUpEnabled(mActivity, enabled);
+ }
+ }
+}
diff --git a/menudrawer/src/main/java/net/simonvt/menudrawer/compat/ActionBarHelperCompat.java b/menudrawer/src/main/java/net/simonvt/menudrawer/compat/ActionBarHelperCompat.java
new file mode 100644
index 00000000..0b602578
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/compat/ActionBarHelperCompat.java
@@ -0,0 +1,107 @@
+package net.simonvt.menudrawer.compat;
+
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import java.lang.reflect.Method;
+
+final class ActionBarHelperCompat {
+
+ private static final String TAG = "ActionBarHelperCompat";
+
+ private ActionBarHelperCompat() {
+ }
+
+ public static void setActionBarUpIndicator(Object info, Activity activity, Drawable drawable, int contentDescRes) {
+ final SetIndicatorInfo sii = (SetIndicatorInfo) info;
+ if (sii.mUpIndicatorView != null) {
+ sii.mUpIndicatorView.setImageDrawable(drawable);
+ final String contentDescription = contentDescRes == 0 ? null : activity.getString(contentDescRes);
+ sii.mUpIndicatorView.setContentDescription(contentDescription);
+ }
+ }
+
+ public static void setActionBarDescription(Object info, Activity activity, int contentDescRes) {
+ final SetIndicatorInfo sii = (SetIndicatorInfo) info;
+ if (sii.mUpIndicatorView != null) {
+ final String contentDescription = contentDescRes == 0 ? null : activity.getString(contentDescRes);
+ sii.mUpIndicatorView.setContentDescription(contentDescription);
+ }
+ }
+
+ public static Drawable getThemeUpIndicator(Object info) {
+ final SetIndicatorInfo sii = (SetIndicatorInfo) info;
+ if (sii.mUpIndicatorView != null) {
+ return sii.mUpIndicatorView.getDrawable();
+ }
+ return null;
+ }
+
+ public static Object getIndicatorInfo(Activity activity) {
+ return new SetIndicatorInfo(activity);
+ }
+
+ public static void setDisplayHomeAsUpEnabled(Object info, boolean enabled) {
+ final SetIndicatorInfo sii = (SetIndicatorInfo) info;
+ if (sii.mHomeAsUpEnabled != null) {
+ try {
+ sii.mHomeAsUpEnabled.invoke(sii.mActionBar, enabled);
+ } catch (Throwable t) {
+ if (ActionBarHelper.DEBUG) {
+ Log.e(TAG, "Unable to call setHomeAsUpEnabled", t);
+ }
+ }
+ }
+ }
+
+ private static class SetIndicatorInfo {
+
+ public ImageView mUpIndicatorView;
+ public Object mActionBar;
+ public Method mHomeAsUpEnabled;
+
+ SetIndicatorInfo(Activity activity) {
+ try {
+ String appPackage = activity.getPackageName();
+
+ try {
+ // Attempt to find ActionBarSherlock up indicator
+ final int homeId = activity.getResources().getIdentifier("abs__home", "id", appPackage);
+ View v = activity.findViewById(homeId);
+ ViewGroup parent = (ViewGroup) v.getParent();
+ final int upId = activity.getResources().getIdentifier("abs__up", "id", appPackage);
+ mUpIndicatorView = (ImageView) parent.findViewById(upId);
+ } catch (Throwable t) {
+ if (ActionBarHelper.DEBUG) {
+ Log.e(TAG, "ABS action bar not found", t);
+ }
+ }
+
+ if (mUpIndicatorView == null) {
+ // Attempt to find AppCompat up indicator
+ final int homeId = activity.getResources().getIdentifier("home", "id", appPackage);
+ View v = activity.findViewById(homeId);
+ ViewGroup parent = (ViewGroup) v.getParent();
+ final int upId = activity.getResources().getIdentifier("up", "id", appPackage);
+ mUpIndicatorView = (ImageView) parent.findViewById(upId);
+ }
+
+ Class supportActivity = activity.getClass();
+ Method getActionBar = supportActivity.getMethod("getSupportActionBar");
+
+ mActionBar = getActionBar.invoke(activity, null);
+ Class supportActionBar = mActionBar.getClass();
+ mHomeAsUpEnabled = supportActionBar.getMethod("setDisplayHomeAsUpEnabled", Boolean.TYPE);
+
+ } catch (Throwable t) {
+ if (ActionBarHelper.DEBUG) {
+ Log.e(TAG, "Unable to init SetIndicatorInfo for ABS", t);
+ }
+ }
+ }
+ }
+}
diff --git a/menudrawer/src/main/java/net/simonvt/menudrawer/compat/ActionBarHelperNative.java b/menudrawer/src/main/java/net/simonvt/menudrawer/compat/ActionBarHelperNative.java
new file mode 100644
index 00000000..89c4b5fa
--- /dev/null
+++ b/menudrawer/src/main/java/net/simonvt/menudrawer/compat/ActionBarHelperNative.java
@@ -0,0 +1,114 @@
+package net.simonvt.menudrawer.compat;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import java.lang.reflect.Method;
+
+final class ActionBarHelperNative {
+
+ private static final String TAG = "ActionBarHelperNative";
+
+ private ActionBarHelperNative() {
+ }
+
+ private static final int[] THEME_ATTRS = new int[] {
+ android.R.attr.homeAsUpIndicator
+ };
+
+ public static void setActionBarUpIndicator(Object info, Activity activity, Drawable drawable, int contentDescRes) {
+
+ final SetIndicatorInfo sii = (SetIndicatorInfo) info;
+ if (sii.setHomeAsUpIndicator != null) {
+ try {
+ final ActionBar actionBar = activity.getActionBar();
+ sii.setHomeAsUpIndicator.invoke(actionBar, drawable);
+ sii.setHomeActionContentDescription.invoke(actionBar, contentDescRes);
+ } catch (Throwable t) {
+ if (ActionBarHelper.DEBUG) Log.e(TAG, "Couldn't set home-as-up indicator via JB-MR2 API", t);
+ }
+ } else if (sii.upIndicatorView != null) {
+ sii.upIndicatorView.setImageDrawable(drawable);
+ } else {
+ if (ActionBarHelper.DEBUG) Log.e(TAG, "Couldn't set home-as-up indicator");
+ }
+ }
+
+ public static void setActionBarDescription(Object info, Activity activity, int contentDescRes) {
+ final SetIndicatorInfo sii = (SetIndicatorInfo) info;
+ if (sii.setHomeAsUpIndicator != null) {
+ try {
+ final ActionBar actionBar = activity.getActionBar();
+ sii.setHomeActionContentDescription.invoke(actionBar, contentDescRes);
+ } catch (Throwable t) {
+ if (ActionBarHelper.DEBUG) Log.e(TAG, "Couldn't set content description via JB-MR2 API", t);
+ }
+ }
+ }
+
+ public static Drawable getThemeUpIndicator(Object info, Activity activity) {
+ final TypedArray a = activity.obtainStyledAttributes(THEME_ATTRS);
+ final Drawable result = a.getDrawable(0);
+ a.recycle();
+ return result;
+ }
+
+ public static Object getIndicatorInfo(Activity activity) {
+ return new SetIndicatorInfo(activity);
+ }
+
+ public static void setDisplayHomeAsUpEnabled(Activity activity, boolean b) {
+ ActionBar actionBar = activity.getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(b);
+ }
+ }
+
+ private static class SetIndicatorInfo {
+
+ public Method setHomeAsUpIndicator;
+ public Method setHomeActionContentDescription;
+ public ImageView upIndicatorView;
+
+ SetIndicatorInfo(Activity activity) {
+ try {
+ setHomeAsUpIndicator = ActionBar.class.getDeclaredMethod("setHomeAsUpIndicator", Drawable.class);
+ setHomeActionContentDescription = ActionBar.class.getDeclaredMethod(
+ "setHomeActionContentDescription", Integer.TYPE);
+
+ // If we got the method we won't need the stuff below.
+ return;
+ } catch (Throwable t) {
+ // Oh well. We'll use the other mechanism below instead.
+ }
+
+ final View home = activity.findViewById(android.R.id.home);
+ if (home == null) {
+ // Action bar doesn't have a known configuration, an OEM messed with things.
+ return;
+ }
+
+ final ViewGroup parent = (ViewGroup) home.getParent();
+ final int childCount = parent.getChildCount();
+ if (childCount != 2) {
+ // No idea which one will be the right one, an OEM messed with things.
+ return;
+ }
+
+ final View first = parent.getChildAt(0);
+ final View second = parent.getChildAt(1);
+ final View up = first.getId() == android.R.id.home ? second : first;
+
+ if (up instanceof ImageView) {
+ // Jackpot! (Probably...)
+ upIndicatorView = (ImageView) up;
+ }
+ }
+ }
+}
diff --git a/menudrawer/src/main/res/values/attrs.xml b/menudrawer/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..7317155f
--- /dev/null
+++ b/menudrawer/src/main/res/values/attrs.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/menudrawer/src/main/res/values/colors.xml b/menudrawer/src/main/res/values/colors.xml
new file mode 100644
index 00000000..2866c6ab
--- /dev/null
+++ b/menudrawer/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+
+ #FF555555
+
+
diff --git a/menudrawer/src/main/res/values/ids.xml b/menudrawer/src/main/res/values/ids.xml
new file mode 100644
index 00000000..197b63cd
--- /dev/null
+++ b/menudrawer/src/main/res/values/ids.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/menudrawer/src/main/res/values/strings.xml b/menudrawer/src/main/res/values/strings.xml
new file mode 100644
index 00000000..27aacbb2
--- /dev/null
+++ b/menudrawer/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+
+
+
+ Close drawer
+
+ Open drawer
+
+
diff --git a/menudrawer/src/main/res/values/styles.xml b/menudrawer/src/main/res/values/styles.xml
new file mode 100644
index 00000000..9b3bed4e
--- /dev/null
+++ b/menudrawer/src/main/res/values/styles.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/pulltorefresh/build.gradle b/pulltorefresh/build.gradle
new file mode 100644
index 00000000..bc827b38
--- /dev/null
+++ b/pulltorefresh/build.gradle
@@ -0,0 +1,18 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 22
+ buildToolsVersion "22.0.1"
+
+ defaultConfig {
+ minSdkVersion 7
+ targetSdkVersion 22
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ }
+ }
+}
diff --git a/pulltorefresh/pulltorefresh.iml b/pulltorefresh/pulltorefresh.iml
new file mode 100644
index 00000000..6ec05180
--- /dev/null
+++ b/pulltorefresh/pulltorefresh.iml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pulltorefresh/src/main/AndroidManifest.xml b/pulltorefresh/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..6382ef54
--- /dev/null
+++ b/pulltorefresh/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/ILoadingLayout.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/ILoadingLayout.java
new file mode 100644
index 00000000..ff2a9572
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/ILoadingLayout.java
@@ -0,0 +1,57 @@
+package com.handmark.pulltorefresh.library;
+
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+
+public interface ILoadingLayout {
+
+ /**
+ * Set the Last Updated Text. This displayed under the main label when
+ * Pulling
+ *
+ * @param label - Label to set
+ */
+ public void setLastUpdatedLabel(CharSequence label);
+
+ /**
+ * Set the drawable used in the loading layout. This is the same as calling
+ * setLoadingDrawable(drawable, Mode.BOTH)
+ *
+ * @param drawable - Drawable to display
+ */
+ public void setLoadingDrawable(Drawable drawable);
+
+ /**
+ * Set Text to show when the Widget is being Pulled
+ * setPullLabel(releaseLabel, Mode.BOTH)
+ *
+ * @param pullLabel - CharSequence to display
+ */
+ public void setPullLabel(CharSequence pullLabel);
+
+ /**
+ * Set Text to show when the Widget is refreshing
+ * setRefreshingLabel(releaseLabel, Mode.BOTH)
+ *
+ * @param refreshingLabel - CharSequence to display
+ */
+ public void setRefreshingLabel(CharSequence refreshingLabel);
+
+ /**
+ * Set Text to show when the Widget is being pulled, and will refresh when
+ * released. This is the same as calling
+ * setReleaseLabel(releaseLabel, Mode.BOTH)
+ *
+ * @param releaseLabel - CharSequence to display
+ */
+ public void setReleaseLabel(CharSequence releaseLabel);
+
+ /**
+ * Set's the Sets the typeface and style in which the text should be
+ * displayed. Please see
+ * {@link android.widget.TextView#setTypeface(Typeface)
+ * TextView#setTypeface(Typeface)}.
+ */
+ public void setTextTypeface(Typeface tf);
+
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/IPullToRefresh.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/IPullToRefresh.java
new file mode 100644
index 00000000..a06cdd7c
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/IPullToRefresh.java
@@ -0,0 +1,246 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library;
+
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.handmark.pulltorefresh.library.PullToRefreshBase.Mode;
+import com.handmark.pulltorefresh.library.PullToRefreshBase.OnPullEventListener;
+import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener;
+import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener2;
+import com.handmark.pulltorefresh.library.PullToRefreshBase.State;
+
+public interface IPullToRefresh {
+
+ /**
+ * Demos the Pull-to-Refresh functionality to the user so that they are
+ * aware it is there. This could be useful when the user first opens your
+ * app, etc. The animation will only happen if the Refresh View (ListView,
+ * ScrollView, etc) is in a state where a Pull-to-Refresh could occur by a
+ * user's touch gesture (i.e. scrolled to the top/bottom).
+ *
+ * @return true - if the Demo has been started, false if not.
+ */
+ public boolean demo();
+
+ /**
+ * Get the mode that this view is currently in. This is only really useful
+ * when using Mode.BOTH.
+ *
+ * @return Mode that the view is currently in
+ */
+ public Mode getCurrentMode();
+
+ /**
+ * Returns whether the Touch Events are filtered or not. If true is
+ * returned, then the View will only use touch events where the difference
+ * in the Y-axis is greater than the difference in the X-axis. This means
+ * that the View will not interfere when it is used in a horizontal
+ * scrolling View (such as a ViewPager).
+ *
+ * @return boolean - true if the View is filtering Touch Events
+ */
+ public boolean getFilterTouchEvents();
+
+ /**
+ * Returns a proxy object which allows you to call methods on all of the
+ * LoadingLayouts (the Views which show when Pulling/Refreshing).
+ *
+ * You should not keep the result of this method any longer than you need
+ * it.
+ *
+ * @return Object which will proxy any calls you make on it, to all of the
+ * LoadingLayouts.
+ */
+ public ILoadingLayout getLoadingLayoutProxy();
+
+ /**
+ * Returns a proxy object which allows you to call methods on the
+ * LoadingLayouts (the Views which show when Pulling/Refreshing). The actual
+ * LoadingLayout(s) which will be affected, are chosen by the parameters you
+ * give.
+ *
+ * You should not keep the result of this method any longer than you need
+ * it.
+ *
+ * @param includeStart - Whether to include the Start/Header Views
+ * @param includeEnd - Whether to include the End/Footer Views
+ * @return Object which will proxy any calls you make on it, to the
+ * LoadingLayouts included.
+ */
+ public ILoadingLayout getLoadingLayoutProxy(boolean includeStart, boolean includeEnd);
+
+ /**
+ * Get the mode that this view has been set to. If this returns
+ * Mode.BOTH, you can use getCurrentMode() to
+ * check which mode the view is currently in
+ *
+ * @return Mode that the view has been set to
+ */
+ public Mode getMode();
+
+ /**
+ * Get the Wrapped Refreshable View. Anything returned here has already been
+ * added to the content view.
+ *
+ * @return The View which is currently wrapped
+ */
+ public T getRefreshableView();
+
+ /**
+ * Get whether the 'Refreshing' View should be automatically shown when
+ * refreshing. Returns true by default.
+ *
+ * @return - true if the Refreshing View will be show
+ */
+ public boolean getShowViewWhileRefreshing();
+
+ /**
+ * @return - The state that the View is currently in.
+ */
+ public State getState();
+
+ /**
+ * Whether Pull-to-Refresh is enabled
+ *
+ * @return enabled
+ */
+ public boolean isPullToRefreshEnabled();
+
+ /**
+ * Gets whether Overscroll support is enabled. This is different to
+ * Android's standard Overscroll support (the edge-glow) which is available
+ * from GINGERBREAD onwards
+ *
+ * @return true - if both PullToRefresh-OverScroll and Android's inbuilt
+ * OverScroll are enabled
+ */
+ public boolean isPullToRefreshOverScrollEnabled();
+
+ /**
+ * Returns whether the Widget is currently in the Refreshing mState
+ *
+ * @return true if the Widget is currently refreshing
+ */
+ public boolean isRefreshing();
+
+ /**
+ * Returns whether the widget has enabled scrolling on the Refreshable View
+ * while refreshing.
+ *
+ * @return true if the widget has enabled scrolling while refreshing
+ */
+ public boolean isScrollingWhileRefreshingEnabled();
+
+ /**
+ * Mark the current Refresh as complete. Will Reset the UI and hide the
+ * Refreshing View
+ */
+ public void onRefreshComplete();
+
+ /**
+ * Set the Touch Events to be filtered or not. If set to true, then the View
+ * will only use touch events where the difference in the Y-axis is greater
+ * than the difference in the X-axis. This means that the View will not
+ * interfere when it is used in a horizontal scrolling View (such as a
+ * ViewPager), but will restrict which types of finger scrolls will trigger
+ * the View.
+ *
+ * @param filterEvents - true if you want to filter Touch Events. Default is
+ * true.
+ */
+ public void setFilterTouchEvents(boolean filterEvents);
+
+ /**
+ * Set the mode of Pull-to-Refresh that this view will use.
+ *
+ * @param mode - Mode to set the View to
+ */
+ public void setMode(Mode mode);
+
+ /**
+ * Set OnPullEventListener for the Widget
+ *
+ * @param listener - Listener to be used when the Widget has a pull event to
+ * propogate.
+ */
+ public void setOnPullEventListener(OnPullEventListener listener);
+
+ /**
+ * Set OnRefreshListener for the Widget
+ *
+ * @param listener - Listener to be used when the Widget is set to Refresh
+ */
+ public void setOnRefreshListener(OnRefreshListener listener);
+
+ /**
+ * Set OnRefreshListener for the Widget
+ *
+ * @param listener - Listener to be used when the Widget is set to Refresh
+ */
+ public void setOnRefreshListener(OnRefreshListener2 listener);
+
+ /**
+ * Sets whether Overscroll support is enabled. This is different to
+ * Android's standard Overscroll support (the edge-glow). This setting only
+ * takes effect when running on device with Android v2.3 or greater.
+ *
+ * @param enabled - true if you want Overscroll enabled
+ */
+ public void setPullToRefreshOverScrollEnabled(boolean enabled);
+
+ /**
+ * Sets the Widget to be in the refresh state. The UI will be updated to
+ * show the 'Refreshing' view, and be scrolled to show such.
+ */
+ public void setRefreshing();
+
+ /**
+ * Sets the Widget to be in the refresh state. The UI will be updated to
+ * show the 'Refreshing' view.
+ *
+ * @param doScroll - true if you want to force a scroll to the Refreshing
+ * view.
+ */
+ public void setRefreshing(boolean doScroll);
+
+ /**
+ * Sets the Animation Interpolator that is used for animated scrolling.
+ * Defaults to a DecelerateInterpolator
+ *
+ * @param interpolator - Interpolator to use
+ */
+ public void setScrollAnimationInterpolator(Interpolator interpolator);
+
+ /**
+ * By default the Widget disables scrolling on the Refreshable View while
+ * refreshing. This method can change this behaviour.
+ *
+ * @param scrollingWhileRefreshingEnabled - true if you want to enable
+ * scrolling while refreshing
+ */
+ public void setScrollingWhileRefreshingEnabled(boolean scrollingWhileRefreshingEnabled);
+
+ /**
+ * A mutator to enable/disable whether the 'Refreshing' View should be
+ * automatically shown when refreshing.
+ *
+ * @param showView
+ */
+ public void setShowViewWhileRefreshing(boolean showView);
+
+}
\ No newline at end of file
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/LoadingLayoutProxy.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/LoadingLayoutProxy.java
new file mode 100644
index 00000000..5f76645d
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/LoadingLayoutProxy.java
@@ -0,0 +1,73 @@
+package com.handmark.pulltorefresh.library;
+
+import java.util.HashSet;
+
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+
+import com.handmark.pulltorefresh.library.internal.LoadingLayout;
+
+public class LoadingLayoutProxy implements ILoadingLayout {
+
+ private final HashSet mLoadingLayouts;
+
+ LoadingLayoutProxy() {
+ mLoadingLayouts = new HashSet();
+ }
+
+ /**
+ * This allows you to add extra LoadingLayout instances to this proxy. This
+ * is only necessary if you keep your own instances, and want to have them
+ * included in any
+ * {@link PullToRefreshBase#createLoadingLayoutProxy(boolean, boolean)
+ * createLoadingLayoutProxy(...)} calls.
+ *
+ * @param layout - LoadingLayout to have included.
+ */
+ public void addLayout(LoadingLayout layout) {
+ if (null != layout) {
+ mLoadingLayouts.add(layout);
+ }
+ }
+
+ @Override
+ public void setLastUpdatedLabel(CharSequence label) {
+ for (LoadingLayout layout : mLoadingLayouts) {
+ layout.setLastUpdatedLabel(label);
+ }
+ }
+
+ @Override
+ public void setLoadingDrawable(Drawable drawable) {
+ for (LoadingLayout layout : mLoadingLayouts) {
+ layout.setLoadingDrawable(drawable);
+ }
+ }
+
+ @Override
+ public void setRefreshingLabel(CharSequence refreshingLabel) {
+ for (LoadingLayout layout : mLoadingLayouts) {
+ layout.setRefreshingLabel(refreshingLabel);
+ }
+ }
+
+ @Override
+ public void setPullLabel(CharSequence label) {
+ for (LoadingLayout layout : mLoadingLayouts) {
+ layout.setPullLabel(label);
+ }
+ }
+
+ @Override
+ public void setReleaseLabel(CharSequence label) {
+ for (LoadingLayout layout : mLoadingLayouts) {
+ layout.setReleaseLabel(label);
+ }
+ }
+
+ public void setTextTypeface(Typeface tf) {
+ for (LoadingLayout layout : mLoadingLayouts) {
+ layout.setTextTypeface(tf);
+ }
+ }
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/OverscrollHelper.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/OverscrollHelper.java
new file mode 100644
index 00000000..52a20de2
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/OverscrollHelper.java
@@ -0,0 +1,178 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library;
+
+import android.annotation.TargetApi;
+import android.util.Log;
+import android.view.View;
+
+import com.handmark.pulltorefresh.library.PullToRefreshBase.Mode;
+import com.handmark.pulltorefresh.library.PullToRefreshBase.State;
+
+@TargetApi(9)
+public final class OverscrollHelper {
+
+ static final String LOG_TAG = "OverscrollHelper";
+ static final float DEFAULT_OVERSCROLL_SCALE = 1f;
+
+ /**
+ * Helper method for Overscrolling that encapsulates all of the necessary
+ * function.
+ *
+ * This should only be used on AdapterView's such as ListView as it just
+ * calls through to overScrollBy() with the scrollRange = 0. AdapterView's
+ * do not have a scroll range (i.e. getScrollY() doesn't work).
+ *
+ * @param view - PullToRefreshView that is calling this.
+ * @param deltaX - Change in X in pixels, passed through from from
+ * overScrollBy call
+ * @param scrollX - Current X scroll value in pixels before applying deltaY,
+ * passed through from from overScrollBy call
+ * @param deltaY - Change in Y in pixels, passed through from from
+ * overScrollBy call
+ * @param scrollY - Current Y scroll value in pixels before applying deltaY,
+ * passed through from from overScrollBy call
+ * @param isTouchEvent - true if this scroll operation is the result of a
+ * touch event, passed through from from overScrollBy call
+ */
+ public static void overScrollBy(final PullToRefreshBase> view, final int deltaX, final int scrollX,
+ final int deltaY, final int scrollY, final boolean isTouchEvent) {
+ overScrollBy(view, deltaX, scrollX, deltaY, scrollY, 0, isTouchEvent);
+ }
+
+ /**
+ * Helper method for Overscrolling that encapsulates all of the necessary
+ * function. This version of the call is used for Views that need to specify
+ * a Scroll Range but scroll back to it's edge correctly.
+ *
+ * @param view - PullToRefreshView that is calling this.
+ * @param deltaX - Change in X in pixels, passed through from from
+ * overScrollBy call
+ * @param scrollX - Current X scroll value in pixels before applying deltaY,
+ * passed through from from overScrollBy call
+ * @param deltaY - Change in Y in pixels, passed through from from
+ * overScrollBy call
+ * @param scrollY - Current Y scroll value in pixels before applying deltaY,
+ * passed through from from overScrollBy call
+ * @param scrollRange - Scroll Range of the View, specifically needed for
+ * ScrollView
+ * @param isTouchEvent - true if this scroll operation is the result of a
+ * touch event, passed through from from overScrollBy call
+ */
+ public static void overScrollBy(final PullToRefreshBase> view, final int deltaX, final int scrollX,
+ final int deltaY, final int scrollY, final int scrollRange, final boolean isTouchEvent) {
+ overScrollBy(view, deltaX, scrollX, deltaY, scrollY, scrollRange, 0, DEFAULT_OVERSCROLL_SCALE, isTouchEvent);
+ }
+
+ /**
+ * Helper method for Overscrolling that encapsulates all of the necessary
+ * function. This is the advanced version of the call.
+ *
+ * @param view - PullToRefreshView that is calling this.
+ * @param deltaX - Change in X in pixels, passed through from from
+ * overScrollBy call
+ * @param scrollX - Current X scroll value in pixels before applying deltaY,
+ * passed through from from overScrollBy call
+ * @param deltaY - Change in Y in pixels, passed through from from
+ * overScrollBy call
+ * @param scrollY - Current Y scroll value in pixels before applying deltaY,
+ * passed through from from overScrollBy call
+ * @param scrollRange - Scroll Range of the View, specifically needed for
+ * ScrollView
+ * @param fuzzyThreshold - Threshold for which the values how fuzzy we
+ * should treat the other values. Needed for WebView as it
+ * doesn't always scroll back to it's edge. 0 = no fuzziness.
+ * @param scaleFactor - Scale Factor for overscroll amount
+ * @param isTouchEvent - true if this scroll operation is the result of a
+ * touch event, passed through from from overScrollBy call
+ */
+ public static void overScrollBy(final PullToRefreshBase> view, final int deltaX, final int scrollX,
+ final int deltaY, final int scrollY, final int scrollRange, final int fuzzyThreshold,
+ final float scaleFactor, final boolean isTouchEvent) {
+
+ final int deltaValue, currentScrollValue, scrollValue;
+ switch (view.getPullToRefreshScrollDirection()) {
+ case HORIZONTAL:
+ deltaValue = deltaX;
+ scrollValue = scrollX;
+ currentScrollValue = view.getScrollX();
+ break;
+ case VERTICAL:
+ default:
+ deltaValue = deltaY;
+ scrollValue = scrollY;
+ currentScrollValue = view.getScrollY();
+ break;
+ }
+
+ // Check that OverScroll is enabled and that we're not currently
+ // refreshing.
+ if (view.isPullToRefreshOverScrollEnabled() && !view.isRefreshing()) {
+ final Mode mode = view.getMode();
+
+ // Check that Pull-to-Refresh is enabled, and the event isn't from
+ // touch
+ if (mode.permitsPullToRefresh() && !isTouchEvent && deltaValue != 0) {
+ final int newScrollValue = (deltaValue + scrollValue);
+
+ if (PullToRefreshBase.DEBUG) {
+ Log.d(LOG_TAG, "OverScroll. DeltaX: " + deltaX + ", ScrollX: " + scrollX + ", DeltaY: " + deltaY
+ + ", ScrollY: " + scrollY + ", NewY: " + newScrollValue + ", ScrollRange: " + scrollRange
+ + ", CurrentScroll: " + currentScrollValue);
+ }
+
+ if (newScrollValue < (0 - fuzzyThreshold)) {
+ // Check the mode supports the overscroll direction, and
+ // then move scroll
+ if (mode.showHeaderLoadingLayout()) {
+ // If we're currently at zero, we're about to start
+ // overscrolling, so change the state
+ if (currentScrollValue == 0) {
+ view.setState(State.OVERSCROLLING);
+ }
+
+ view.setHeaderScroll((int) (scaleFactor * (currentScrollValue + newScrollValue)));
+ }
+ } else if (newScrollValue > (scrollRange + fuzzyThreshold)) {
+ // Check the mode supports the overscroll direction, and
+ // then move scroll
+ if (mode.showFooterLoadingLayout()) {
+ // If we're currently at zero, we're about to start
+ // overscrolling, so change the state
+ if (currentScrollValue == 0) {
+ view.setState(State.OVERSCROLLING);
+ }
+
+ view.setHeaderScroll((int) (scaleFactor * (currentScrollValue + newScrollValue - scrollRange)));
+ }
+ } else if (Math.abs(newScrollValue) <= fuzzyThreshold
+ || Math.abs(newScrollValue - scrollRange) <= fuzzyThreshold) {
+ // Means we've stopped overscrolling, so scroll back to 0
+ view.setState(State.RESET);
+ }
+ } else if (isTouchEvent && State.OVERSCROLLING == view.getState()) {
+ // This condition means that we were overscrolling from a fling,
+ // but the user has touched the View and is now overscrolling
+ // from touch instead. We need to just reset.
+ view.setState(State.RESET);
+ }
+ }
+ }
+
+ static boolean isAndroidOverScrollEnabled(View view) {
+ return view.getOverScrollMode() != View.OVER_SCROLL_NEVER;
+ }
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshAdapterViewBase.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshAdapterViewBase.java
new file mode 100644
index 00000000..cfff8371
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshAdapterViewBase.java
@@ -0,0 +1,475 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.Adapter;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+
+import com.handmark.pulltorefresh.library.internal.EmptyViewMethodAccessor;
+import com.handmark.pulltorefresh.library.internal.IndicatorLayout;
+
+public abstract class PullToRefreshAdapterViewBase extends PullToRefreshBase implements
+ OnScrollListener {
+
+ private static FrameLayout.LayoutParams convertEmptyViewLayoutParams(ViewGroup.LayoutParams lp) {
+ FrameLayout.LayoutParams newLp = null;
+
+ if (null != lp) {
+ newLp = new FrameLayout.LayoutParams(lp);
+
+ if (lp instanceof LinearLayout.LayoutParams) {
+ newLp.gravity = ((LinearLayout.LayoutParams) lp).gravity;
+ } else {
+ newLp.gravity = Gravity.CENTER;
+ }
+ }
+
+ return newLp;
+ }
+
+ private boolean mLastItemVisible;
+ private OnScrollListener mOnScrollListener;
+ private OnLastItemVisibleListener mOnLastItemVisibleListener;
+ private View mEmptyView;
+
+ private IndicatorLayout mIndicatorIvTop;
+ private IndicatorLayout mIndicatorIvBottom;
+
+ private boolean mShowIndicator;
+ private boolean mScrollEmptyView = true;
+
+ public PullToRefreshAdapterViewBase(Context context) {
+ super(context);
+ mRefreshableView.setOnScrollListener(this);
+ }
+
+ public PullToRefreshAdapterViewBase(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mRefreshableView.setOnScrollListener(this);
+ }
+
+ public PullToRefreshAdapterViewBase(Context context, Mode mode) {
+ super(context, mode);
+ mRefreshableView.setOnScrollListener(this);
+ }
+
+ public PullToRefreshAdapterViewBase(Context context, Mode mode, AnimationStyle animStyle) {
+ super(context, mode, animStyle);
+ mRefreshableView.setOnScrollListener(this);
+ }
+
+ /**
+ * Gets whether an indicator graphic should be displayed when the View is in
+ * a state where a Pull-to-Refresh can happen. An example of this state is
+ * when the Adapter View is scrolled to the top and the mode is set to
+ * {@link Mode#PULL_FROM_START}. The default value is true if
+ * {@link PullToRefreshBase#isPullToRefreshOverScrollEnabled()
+ * isPullToRefreshOverScrollEnabled()} returns false.
+ *
+ * @return true if the indicators will be shown
+ */
+ public boolean getShowIndicator() {
+ return mShowIndicator;
+ }
+
+ public final void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount,
+ final int totalItemCount) {
+
+ if (DEBUG) {
+ Log.d(LOG_TAG, "First Visible: " + firstVisibleItem + ". Visible Count: " + visibleItemCount
+ + ". Total Items:" + totalItemCount);
+ }
+
+ /**
+ * Set whether the Last Item is Visible. lastVisibleItemIndex is a
+ * zero-based index, so we minus one totalItemCount to check
+ */
+ if (null != mOnLastItemVisibleListener) {
+ mLastItemVisible = (totalItemCount > 0) && (firstVisibleItem + visibleItemCount >= totalItemCount - 1);
+ }
+
+ // If we're showing the indicator, check positions...
+ if (getShowIndicatorInternal()) {
+ updateIndicatorViewsVisibility();
+ }
+
+ // Finally call OnScrollListener if we have one
+ if (null != mOnScrollListener) {
+ mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
+ }
+ }
+
+ public final void onScrollStateChanged(final AbsListView view, final int state) {
+ /**
+ * Check that the scrolling has stopped, and that the last item is
+ * visible.
+ */
+ if (state == OnScrollListener.SCROLL_STATE_IDLE && null != mOnLastItemVisibleListener && mLastItemVisible) {
+ mOnLastItemVisibleListener.onLastItemVisible();
+ }
+
+ if (null != mOnScrollListener) {
+ mOnScrollListener.onScrollStateChanged(view, state);
+ }
+ }
+
+ /**
+ * Pass-through method for {@link PullToRefreshBase#getRefreshableView()
+ * getRefreshableView()}.
+ * {@link AdapterView#setAdapter(android.widget.Adapter)}
+ * setAdapter(adapter)}. This is just for convenience!
+ *
+ * @param adapter - Adapter to set
+ */
+ public void setAdapter(ListAdapter adapter) {
+ ((AdapterView) mRefreshableView).setAdapter(adapter);
+ }
+
+ /**
+ * Sets the Empty View to be used by the Adapter View.
+ *
+ * We need it handle it ourselves so that we can Pull-to-Refresh when the
+ * Empty View is shown.
+ *
+ * Please note, you do not usually need to call this method
+ * yourself. Calling setEmptyView on the AdapterView will automatically call
+ * this method and set everything up. This includes when the Android
+ * Framework automatically sets the Empty View based on it's ID.
+ *
+ * @param newEmptyView - Empty View to be used
+ */
+ public final void setEmptyView(View newEmptyView) {
+ FrameLayout refreshableViewWrapper = getRefreshableViewWrapper();
+
+ if (null != newEmptyView) {
+ // New view needs to be clickable so that Android recognizes it as a
+ // target for Touch Events
+ newEmptyView.setClickable(true);
+
+ ViewParent newEmptyViewParent = newEmptyView.getParent();
+ if (null != newEmptyViewParent && newEmptyViewParent instanceof ViewGroup) {
+ ((ViewGroup) newEmptyViewParent).removeView(newEmptyView);
+ }
+
+ // We need to convert any LayoutParams so that it works in our
+ // FrameLayout
+ FrameLayout.LayoutParams lp = convertEmptyViewLayoutParams(newEmptyView.getLayoutParams());
+ if (null != lp) {
+ refreshableViewWrapper.addView(newEmptyView, lp);
+ } else {
+ refreshableViewWrapper.addView(newEmptyView);
+ }
+ }
+
+ if (mRefreshableView instanceof EmptyViewMethodAccessor) {
+ ((EmptyViewMethodAccessor) mRefreshableView).setEmptyViewInternal(newEmptyView);
+ } else {
+ mRefreshableView.setEmptyView(newEmptyView);
+ }
+ mEmptyView = newEmptyView;
+ }
+
+ /**
+ * Pass-through method for {@link PullToRefreshBase#getRefreshableView()
+ * getRefreshableView()}.
+ * {@link AdapterView#setOnItemClickListener(OnItemClickListener)
+ * setOnItemClickListener(listener)}. This is just for convenience!
+ *
+ * @param listener - OnItemClickListener to use
+ */
+ public void setOnItemClickListener(OnItemClickListener listener) {
+ mRefreshableView.setOnItemClickListener(listener);
+ }
+
+ public final void setOnLastItemVisibleListener(OnLastItemVisibleListener listener) {
+ mOnLastItemVisibleListener = listener;
+ }
+
+ public final void setOnScrollListener(OnScrollListener listener) {
+ mOnScrollListener = listener;
+ }
+
+ public final void setScrollEmptyView(boolean doScroll) {
+ mScrollEmptyView = doScroll;
+ }
+
+ /**
+ * Sets whether an indicator graphic should be displayed when the View is in
+ * a state where a Pull-to-Refresh can happen. An example of this state is
+ * when the Adapter View is scrolled to the top and the mode is set to
+ * {@link Mode#PULL_FROM_START}
+ *
+ * @param showIndicator - true if the indicators should be shown.
+ */
+ public void setShowIndicator(boolean showIndicator) {
+ mShowIndicator = showIndicator;
+
+ if (getShowIndicatorInternal()) {
+ // If we're set to Show Indicator, add/update them
+ addIndicatorViews();
+ } else {
+ // If not, then remove then
+ removeIndicatorViews();
+ }
+ }
+
+ ;
+
+ @Override
+ protected void onPullToRefresh() {
+ super.onPullToRefresh();
+
+ if (getShowIndicatorInternal()) {
+ switch (getCurrentMode()) {
+ case PULL_FROM_END:
+ mIndicatorIvBottom.pullToRefresh();
+ break;
+ case PULL_FROM_START:
+ mIndicatorIvTop.pullToRefresh();
+ break;
+ default:
+ // NO-OP
+ break;
+ }
+ }
+ }
+
+ protected void onRefreshing(boolean doScroll) {
+ super.onRefreshing(doScroll);
+
+ if (getShowIndicatorInternal()) {
+ updateIndicatorViewsVisibility();
+ }
+ }
+
+ @Override
+ protected void onReleaseToRefresh() {
+ super.onReleaseToRefresh();
+
+ if (getShowIndicatorInternal()) {
+ switch (getCurrentMode()) {
+ case PULL_FROM_END:
+ mIndicatorIvBottom.releaseToRefresh();
+ break;
+ case PULL_FROM_START:
+ mIndicatorIvTop.releaseToRefresh();
+ break;
+ default:
+ // NO-OP
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+
+ if (getShowIndicatorInternal()) {
+ updateIndicatorViewsVisibility();
+ }
+ }
+
+ @Override
+ protected void handleStyledAttributes(TypedArray a) {
+ // Set Show Indicator to the XML value, or default value
+ mShowIndicator = a.getBoolean(R.styleable.PullToRefresh_ptrShowIndicator, !isPullToRefreshOverScrollEnabled());
+ }
+
+ protected boolean isReadyForPullStart() {
+ return isFirstItemVisible();
+ }
+
+ protected boolean isReadyForPullEnd() {
+ return isLastItemVisible();
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ if (null != mEmptyView && !mScrollEmptyView) {
+ mEmptyView.scrollTo(-l, -t);
+ }
+ }
+
+ @Override
+ protected void updateUIForMode() {
+ super.updateUIForMode();
+
+ // Check Indicator Views consistent with new Mode
+ if (getShowIndicatorInternal()) {
+ addIndicatorViews();
+ } else {
+ removeIndicatorViews();
+ }
+ }
+
+ private void addIndicatorViews() {
+ Mode mode = getMode();
+ FrameLayout refreshableViewWrapper = getRefreshableViewWrapper();
+
+ if (mode.showHeaderLoadingLayout() && null == mIndicatorIvTop) {
+ // If the mode can pull down, and we don't have one set already
+ mIndicatorIvTop = new IndicatorLayout(getContext(), Mode.PULL_FROM_START);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ params.rightMargin = getResources().getDimensionPixelSize(R.dimen.indicator_right_padding);
+ params.gravity = Gravity.TOP | Gravity.RIGHT;
+ refreshableViewWrapper.addView(mIndicatorIvTop, params);
+
+ } else if (!mode.showHeaderLoadingLayout() && null != mIndicatorIvTop) {
+ // If we can't pull down, but have a View then remove it
+ refreshableViewWrapper.removeView(mIndicatorIvTop);
+ mIndicatorIvTop = null;
+ }
+
+ if (mode.showFooterLoadingLayout() && null == mIndicatorIvBottom) {
+ // If the mode can pull down, and we don't have one set already
+ mIndicatorIvBottom = new IndicatorLayout(getContext(), Mode.PULL_FROM_END);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ params.rightMargin = getResources().getDimensionPixelSize(R.dimen.indicator_right_padding);
+ params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
+ refreshableViewWrapper.addView(mIndicatorIvBottom, params);
+
+ } else if (!mode.showFooterLoadingLayout() && null != mIndicatorIvBottom) {
+ // If we can't pull down, but have a View then remove it
+ refreshableViewWrapper.removeView(mIndicatorIvBottom);
+ mIndicatorIvBottom = null;
+ }
+ }
+
+ private boolean getShowIndicatorInternal() {
+ return mShowIndicator && isPullToRefreshEnabled();
+ }
+
+ private boolean isFirstItemVisible() {
+ final Adapter adapter = mRefreshableView.getAdapter();
+
+ if (null == adapter || adapter.isEmpty()) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "isFirstItemVisible. Empty View.");
+ }
+ return true;
+
+ } else {
+
+ /**
+ * This check should really just be:
+ * mRefreshableView.getFirstVisiblePosition() == 0, but PtRListView
+ * internally use a HeaderView which messes the positions up. For
+ * now we'll just add one to account for it and rely on the inner
+ * condition which checks getTop().
+ */
+ if (mRefreshableView.getFirstVisiblePosition() <= 1) {
+ final View firstVisibleChild = mRefreshableView.getChildAt(0);
+ if (firstVisibleChild != null) {
+ return firstVisibleChild.getTop() >= mRefreshableView.getTop();
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private boolean isLastItemVisible() {
+ final Adapter adapter = mRefreshableView.getAdapter();
+
+ if (null == adapter || adapter.isEmpty()) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "isLastItemVisible. Empty View.");
+ }
+ return true;
+ } else {
+ final int lastItemPosition = mRefreshableView.getCount() - 1;
+ final int lastVisiblePosition = mRefreshableView.getLastVisiblePosition();
+
+ if (DEBUG) {
+ Log.d(LOG_TAG, "isLastItemVisible. Last Item Position: " + lastItemPosition + " Last Visible Pos: "
+ + lastVisiblePosition);
+ }
+
+ /**
+ * This check should really just be: lastVisiblePosition ==
+ * lastItemPosition, but PtRListView internally uses a FooterView
+ * which messes the positions up. For me we'll just subtract one to
+ * account for it and rely on the inner condition which checks
+ * getBottom().
+ */
+ if (lastVisiblePosition >= lastItemPosition - 1) {
+ final int childIndex = lastVisiblePosition - mRefreshableView.getFirstVisiblePosition();
+ final View lastVisibleChild = mRefreshableView.getChildAt(childIndex);
+ if (lastVisibleChild != null) {
+ return lastVisibleChild.getBottom() <= mRefreshableView.getBottom();
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private void removeIndicatorViews() {
+ if (null != mIndicatorIvTop) {
+ getRefreshableViewWrapper().removeView(mIndicatorIvTop);
+ mIndicatorIvTop = null;
+ }
+
+ if (null != mIndicatorIvBottom) {
+ getRefreshableViewWrapper().removeView(mIndicatorIvBottom);
+ mIndicatorIvBottom = null;
+ }
+ }
+
+ private void updateIndicatorViewsVisibility() {
+ if (null != mIndicatorIvTop) {
+ if (!isRefreshing() && isReadyForPullStart()) {
+ if (!mIndicatorIvTop.isVisible()) {
+ mIndicatorIvTop.show();
+ }
+ } else {
+ if (mIndicatorIvTop.isVisible()) {
+ mIndicatorIvTop.hide();
+ }
+ }
+ }
+
+ if (null != mIndicatorIvBottom) {
+ if (!isRefreshing() && isReadyForPullEnd()) {
+ if (!mIndicatorIvBottom.isVisible()) {
+ mIndicatorIvBottom.show();
+ }
+ } else {
+ if (mIndicatorIvBottom.isVisible()) {
+ mIndicatorIvBottom.hide();
+ }
+ }
+ }
+ }
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshBase.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshBase.java
new file mode 100644
index 00000000..e76b234f
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshBase.java
@@ -0,0 +1,1653 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.handmark.pulltorefresh.library.internal.FlipLoadingLayout;
+import com.handmark.pulltorefresh.library.internal.LoadingLayout;
+import com.handmark.pulltorefresh.library.internal.RotateLoadingLayout;
+import com.handmark.pulltorefresh.library.internal.Utils;
+import com.handmark.pulltorefresh.library.internal.ViewCompat;
+
+public abstract class PullToRefreshBase extends LinearLayout implements IPullToRefresh {
+
+ // ===========================================================
+ // Constants
+ // ===========================================================
+
+ static final boolean DEBUG = true;
+
+ static final boolean USE_HW_LAYERS = false;
+
+ static final String LOG_TAG = "PullToRefresh";
+
+ static final float FRICTION = 2.0f;
+
+ public static final int SMOOTH_SCROLL_DURATION_MS = 200;
+ public static final int SMOOTH_SCROLL_LONG_DURATION_MS = 325;
+ static final int DEMO_SCROLL_INTERVAL = 225;
+
+ static final String STATE_STATE = "ptr_state";
+ static final String STATE_MODE = "ptr_mode";
+ static final String STATE_CURRENT_MODE = "ptr_current_mode";
+ static final String STATE_SCROLLING_REFRESHING_ENABLED = "ptr_disable_scrolling";
+ static final String STATE_SHOW_REFRESHING_VIEW = "ptr_show_refreshing_view";
+ static final String STATE_SUPER = "ptr_super";
+
+ // ===========================================================
+ // Fields
+ // ===========================================================
+
+ private int mTouchSlop;
+ private float mLastMotionX, mLastMotionY;
+ private float mInitialMotionX, mInitialMotionY;
+
+ private boolean mIsBeingDragged = false;
+ private State mState = State.RESET;
+ private Mode mMode = Mode.getDefault();
+
+ private Mode mCurrentMode;
+ T mRefreshableView;
+ private FrameLayout mRefreshableViewWrapper;
+
+ private boolean mShowViewWhileRefreshing = true;
+ private boolean mScrollingWhileRefreshingEnabled = false;
+ private boolean mFilterTouchEvents = true;
+ private boolean mOverScrollEnabled = true;
+ private boolean mLayoutVisibilityChangesEnabled = true;
+
+ private Interpolator mScrollAnimationInterpolator;
+ private AnimationStyle mLoadingAnimationStyle = AnimationStyle.getDefault();
+
+ private LoadingLayout mHeaderLayout;
+ private LoadingLayout mFooterLayout;
+
+ private OnRefreshListener mOnRefreshListener;
+ private OnRefreshListener2 mOnRefreshListener2;
+ private OnPullEventListener mOnPullEventListener;
+
+ private SmoothScrollRunnable mCurrentSmoothScrollRunnable;
+
+ // ===========================================================
+ // Constructors
+ // ===========================================================
+
+ public PullToRefreshBase(Context context) {
+ super(context);
+ init(context, null);
+ }
+
+ public PullToRefreshBase(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ public PullToRefreshBase(Context context, Mode mode) {
+ super(context);
+ mMode = mode;
+ init(context, null);
+ }
+
+ public PullToRefreshBase(Context context, Mode mode, AnimationStyle animStyle) {
+ super(context);
+ mMode = mode;
+ mLoadingAnimationStyle = animStyle;
+ init(context, null);
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "addView: " + child.getClass().getSimpleName());
+ }
+
+ final T refreshableView = getRefreshableView();
+
+ if (refreshableView instanceof ViewGroup) {
+ ((ViewGroup) refreshableView).addView(child, index, params);
+ } else {
+ throw new UnsupportedOperationException("Refreshable View is not a ViewGroup so can't addView");
+ }
+ }
+
+ @Override
+ public final boolean demo() {
+ if (mMode.showHeaderLoadingLayout() && isReadyForPullStart()) {
+ smoothScrollToAndBack(-getHeaderSize() * 2);
+ return true;
+ } else if (mMode.showFooterLoadingLayout() && isReadyForPullEnd()) {
+ smoothScrollToAndBack(getFooterSize() * 2);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public final Mode getCurrentMode() {
+ return mCurrentMode;
+ }
+
+ @Override
+ public final boolean getFilterTouchEvents() {
+ return mFilterTouchEvents;
+ }
+
+ @Override
+ public final ILoadingLayout getLoadingLayoutProxy() {
+ return getLoadingLayoutProxy(true, true);
+ }
+
+ @Override
+ public final ILoadingLayout getLoadingLayoutProxy(boolean includeStart, boolean includeEnd) {
+ return createLoadingLayoutProxy(includeStart, includeEnd);
+ }
+
+ @Override
+ public final Mode getMode() {
+ return mMode;
+ }
+
+ @Override
+ public final T getRefreshableView() {
+ return mRefreshableView;
+ }
+
+ @Override
+ public final boolean getShowViewWhileRefreshing() {
+ return mShowViewWhileRefreshing;
+ }
+
+ @Override
+ public final State getState() {
+ return mState;
+ }
+
+ /**
+ * @deprecated See {@link #isScrollingWhileRefreshingEnabled()}.
+ */
+ public final boolean isDisableScrollingWhileRefreshing() {
+ return !isScrollingWhileRefreshingEnabled();
+ }
+
+ @Override
+ public final boolean isPullToRefreshEnabled() {
+ return mMode.permitsPullToRefresh();
+ }
+
+ @Override
+ public final boolean isPullToRefreshOverScrollEnabled() {
+ return VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD && mOverScrollEnabled
+ && OverscrollHelper.isAndroidOverScrollEnabled(mRefreshableView);
+ }
+
+ @Override
+ public final boolean isRefreshing() {
+ return mState == State.REFRESHING || mState == State.MANUAL_REFRESHING;
+ }
+
+ @Override
+ public final boolean isScrollingWhileRefreshingEnabled() {
+ return mScrollingWhileRefreshingEnabled;
+ }
+
+ @Override
+ public final boolean onInterceptTouchEvent(MotionEvent event) {
+
+ if (!isPullToRefreshEnabled()) {
+ return false;
+ }
+
+ final int action = event.getAction();
+
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ mIsBeingDragged = false;
+ return false;
+ }
+
+ if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
+ return true;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_MOVE: {
+ // If we're refreshing, and the flag is set. Eat all MOVE events
+ if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
+ return true;
+ }
+
+ if (isReadyForPull()) {
+ final float y = event.getY(), x = event.getX();
+ final float diff, oppositeDiff, absDiff;
+
+ // We need to use the correct values, based on scroll
+ // direction
+ switch (getPullToRefreshScrollDirection()) {
+ case HORIZONTAL:
+ diff = x - mLastMotionX;
+ oppositeDiff = y - mLastMotionY;
+ break;
+ case VERTICAL:
+ default:
+ diff = y - mLastMotionY;
+ oppositeDiff = x - mLastMotionX;
+ break;
+ }
+ absDiff = Math.abs(diff);
+
+ if (absDiff > mTouchSlop && (!mFilterTouchEvents || absDiff > Math.abs(oppositeDiff))) {
+ if (mMode.showHeaderLoadingLayout() && diff >= 1f && isReadyForPullStart()) {
+ mLastMotionY = y;
+ mLastMotionX = x;
+ mIsBeingDragged = true;
+ if (mMode == Mode.BOTH) {
+ mCurrentMode = Mode.PULL_FROM_START;
+ }
+ } else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd()) {
+ mLastMotionY = y;
+ mLastMotionX = x;
+ mIsBeingDragged = true;
+ if (mMode == Mode.BOTH) {
+ mCurrentMode = Mode.PULL_FROM_END;
+ }
+ }
+ }
+ }
+ break;
+ }
+ case MotionEvent.ACTION_DOWN: {
+ if (isReadyForPull()) {
+ mLastMotionY = mInitialMotionY = event.getY();
+ mLastMotionX = mInitialMotionX = event.getX();
+ mIsBeingDragged = false;
+ }
+ break;
+ }
+ }
+
+ return mIsBeingDragged;
+ }
+
+ @Override
+ public final void onRefreshComplete() {
+ if (isRefreshing()) {
+ setState(State.RESET);
+ }
+ }
+
+ @Override
+ public final boolean onTouchEvent(MotionEvent event) {
+
+ if (!isPullToRefreshEnabled()) {
+ return false;
+ }
+
+ // If we're refreshing, and the flag is set. Eat the event
+ if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
+ return true;
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) {
+ return false;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_MOVE: {
+ if (mIsBeingDragged) {
+ mLastMotionY = event.getY();
+ mLastMotionX = event.getX();
+ pullEvent();
+ return true;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_DOWN: {
+ if (isReadyForPull()) {
+ mLastMotionY = mInitialMotionY = event.getY();
+ mLastMotionX = mInitialMotionX = event.getX();
+ return true;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ if (mIsBeingDragged) {
+ mIsBeingDragged = false;
+
+ if (mState == State.RELEASE_TO_REFRESH
+ && (null != mOnRefreshListener || null != mOnRefreshListener2)) {
+ setState(State.REFRESHING, true);
+ return true;
+ }
+
+ // If we're already refreshing, just scroll back to the top
+ if (isRefreshing()) {
+ smoothScrollTo(0);
+ return true;
+ }
+
+ // If we haven't returned by here, then we're not in a state
+ // to pull, so just reset
+ setState(State.RESET);
+
+ return true;
+ }
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ public final void setScrollingWhileRefreshingEnabled(boolean allowScrollingWhileRefreshing) {
+ mScrollingWhileRefreshingEnabled = allowScrollingWhileRefreshing;
+ }
+
+ /**
+ * @deprecated See {@link #setScrollingWhileRefreshingEnabled(boolean)}
+ */
+ public void setDisableScrollingWhileRefreshing(boolean disableScrollingWhileRefreshing) {
+ setScrollingWhileRefreshingEnabled(!disableScrollingWhileRefreshing);
+ }
+
+ @Override
+ public final void setFilterTouchEvents(boolean filterEvents) {
+ mFilterTouchEvents = filterEvents;
+ }
+
+ /**
+ * @deprecated You should now call this method on the result of
+ * {@link #getLoadingLayoutProxy()}.
+ */
+ public void setLastUpdatedLabel(CharSequence label) {
+ getLoadingLayoutProxy().setLastUpdatedLabel(label);
+ }
+
+ /**
+ * @deprecated You should now call this method on the result of
+ * {@link #getLoadingLayoutProxy()}.
+ */
+ public void setLoadingDrawable(Drawable drawable) {
+ getLoadingLayoutProxy().setLoadingDrawable(drawable);
+ }
+
+ /**
+ * @deprecated You should now call this method on the result of
+ * {@link #getLoadingLayoutProxy(boolean, boolean)}.
+ */
+ public void setLoadingDrawable(Drawable drawable, Mode mode) {
+ getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()).setLoadingDrawable(
+ drawable);
+ }
+
+ @Override
+ public void setLongClickable(boolean longClickable) {
+ getRefreshableView().setLongClickable(longClickable);
+ }
+
+ @Override
+ public final void setMode(Mode mode) {
+ if (mode != mMode) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Setting mode to: " + mode);
+ }
+ mMode = mode;
+ updateUIForMode();
+ }
+ }
+
+ public void setOnPullEventListener(OnPullEventListener listener) {
+ mOnPullEventListener = listener;
+ }
+
+ @Override
+ public final void setOnRefreshListener(OnRefreshListener listener) {
+ mOnRefreshListener = listener;
+ mOnRefreshListener2 = null;
+ }
+
+ @Override
+ public final void setOnRefreshListener(OnRefreshListener2 listener) {
+ mOnRefreshListener2 = listener;
+ mOnRefreshListener = null;
+ }
+
+ /**
+ * @deprecated You should now call this method on the result of
+ * {@link #getLoadingLayoutProxy()}.
+ */
+ public void setPullLabel(CharSequence pullLabel) {
+ getLoadingLayoutProxy().setPullLabel(pullLabel);
+ }
+
+ /**
+ * @deprecated You should now call this method on the result of
+ * {@link #getLoadingLayoutProxy(boolean, boolean)}.
+ */
+ public void setPullLabel(CharSequence pullLabel, Mode mode) {
+ getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()).setPullLabel(pullLabel);
+ }
+
+ /**
+ * @param enable Whether Pull-To-Refresh should be used
+ * @deprecated This simple calls setMode with an appropriate mode based on
+ * the passed value.
+ */
+ public final void setPullToRefreshEnabled(boolean enable) {
+ setMode(enable ? Mode.getDefault() : Mode.DISABLED);
+ }
+
+ @Override
+ public final void setPullToRefreshOverScrollEnabled(boolean enabled) {
+ mOverScrollEnabled = enabled;
+ }
+
+ @Override
+ public final void setRefreshing() {
+ setRefreshing(true);
+ }
+
+ @Override
+ public final void setRefreshing(boolean doScroll) {
+ if (!isRefreshing()) {
+ setState(State.MANUAL_REFRESHING, doScroll);
+ }
+ }
+
+ /**
+ * @deprecated You should now call this method on the result of
+ * {@link #getLoadingLayoutProxy()}.
+ */
+ public void setRefreshingLabel(CharSequence refreshingLabel) {
+ getLoadingLayoutProxy().setRefreshingLabel(refreshingLabel);
+ }
+
+ /**
+ * @deprecated You should now call this method on the result of
+ * {@link #getLoadingLayoutProxy(boolean, boolean)}.
+ */
+ public void setRefreshingLabel(CharSequence refreshingLabel, Mode mode) {
+ getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()).setRefreshingLabel(
+ refreshingLabel);
+ }
+
+ /**
+ * @deprecated You should now call this method on the result of
+ * {@link #getLoadingLayoutProxy()}.
+ */
+ public void setReleaseLabel(CharSequence releaseLabel) {
+ setReleaseLabel(releaseLabel, Mode.BOTH);
+ }
+
+ /**
+ * @deprecated You should now call this method on the result of
+ * {@link #getLoadingLayoutProxy(boolean, boolean)}.
+ */
+ public void setReleaseLabel(CharSequence releaseLabel, Mode mode) {
+ getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()).setReleaseLabel(
+ releaseLabel);
+ }
+
+ public void setScrollAnimationInterpolator(Interpolator interpolator) {
+ mScrollAnimationInterpolator = interpolator;
+ }
+
+ @Override
+ public final void setShowViewWhileRefreshing(boolean showView) {
+ mShowViewWhileRefreshing = showView;
+ }
+
+ /**
+ * @return Either {@link Orientation#VERTICAL} or
+ * {@link Orientation#HORIZONTAL} depending on the scroll direction.
+ */
+ public abstract Orientation getPullToRefreshScrollDirection();
+
+ final void setState(State state, final boolean... params) {
+ mState = state;
+ if (DEBUG) {
+ Log.d(LOG_TAG, "State: " + mState.name());
+ }
+
+ switch (mState) {
+ case RESET:
+ onReset();
+ break;
+ case PULL_TO_REFRESH:
+ onPullToRefresh();
+ break;
+ case RELEASE_TO_REFRESH:
+ onReleaseToRefresh();
+ break;
+ case REFRESHING:
+ case MANUAL_REFRESHING:
+ onRefreshing(params[0]);
+ break;
+ case OVERSCROLLING:
+ // NO-OP
+ break;
+ }
+
+ // Call OnPullEventListener
+ if (null != mOnPullEventListener) {
+ mOnPullEventListener.onPullEvent(this, mState, mCurrentMode);
+ }
+ }
+
+ /**
+ * Used internally for adding view. Need because we override addView to
+ * pass-through to the Refreshable View
+ */
+ protected final void addViewInternal(View child, int index, ViewGroup.LayoutParams params) {
+ super.addView(child, index, params);
+ }
+
+ /**
+ * Used internally for adding view. Need because we override addView to
+ * pass-through to the Refreshable View
+ */
+ protected final void addViewInternal(View child, ViewGroup.LayoutParams params) {
+ super.addView(child, -1, params);
+ }
+
+ protected LoadingLayout createLoadingLayout(Context context, Mode mode, TypedArray attrs) {
+ LoadingLayout layout = mLoadingAnimationStyle.createLoadingLayout(context, mode,
+ getPullToRefreshScrollDirection(), attrs);
+ layout.setVisibility(View.INVISIBLE);
+ return layout;
+ }
+
+ /**
+ * Used internally for {@link #getLoadingLayoutProxy(boolean, boolean)}.
+ * Allows derivative classes to include any extra LoadingLayouts.
+ */
+ protected LoadingLayoutProxy createLoadingLayoutProxy(final boolean includeStart, final boolean includeEnd) {
+ LoadingLayoutProxy proxy = new LoadingLayoutProxy();
+
+ if (includeStart && mMode.showHeaderLoadingLayout()) {
+ proxy.addLayout(mHeaderLayout);
+ }
+ if (includeEnd && mMode.showFooterLoadingLayout()) {
+ proxy.addLayout(mFooterLayout);
+ }
+
+ return proxy;
+ }
+
+ /**
+ * This is implemented by derived classes to return the created View. If you
+ * need to use a custom View (such as a custom ListView), override this
+ * method and return an instance of your custom class.
+ *
+ * Be sure to set the ID of the view in this method, especially if you're
+ * using a ListActivity or ListFragment.
+ *
+ * @param context Context to create view with
+ * @param attrs AttributeSet from wrapped class. Means that anything you
+ * include in the XML layout declaration will be routed to the
+ * created View
+ * @return New instance of the Refreshable View
+ */
+ protected abstract T createRefreshableView(Context context, AttributeSet attrs);
+
+ protected final void disableLoadingLayoutVisibilityChanges() {
+ mLayoutVisibilityChangesEnabled = false;
+ }
+
+ protected final LoadingLayout getFooterLayout() {
+ return mFooterLayout;
+ }
+
+ protected final int getFooterSize() {
+ return mFooterLayout.getContentSize();
+ }
+
+ protected final LoadingLayout getHeaderLayout() {
+ return mHeaderLayout;
+ }
+
+ protected final int getHeaderSize() {
+ return mHeaderLayout.getContentSize();
+ }
+
+ protected int getPullToRefreshScrollDuration() {
+ return SMOOTH_SCROLL_DURATION_MS;
+ }
+
+ protected int getPullToRefreshScrollDurationLonger() {
+ return SMOOTH_SCROLL_LONG_DURATION_MS;
+ }
+
+ protected FrameLayout getRefreshableViewWrapper() {
+ return mRefreshableViewWrapper;
+ }
+
+ /**
+ * Allows Derivative classes to handle the XML Attrs without creating a
+ * TypedArray themsevles
+ *
+ * @param a - TypedArray of PullToRefresh Attributes
+ */
+ protected void handleStyledAttributes(TypedArray a) {
+ }
+
+ /**
+ * Implemented by derived class to return whether the View is in a state
+ * where the user can Pull to Refresh by scrolling from the end.
+ *
+ * @return true if the View is currently in the correct state (for example,
+ * bottom of a ListView)
+ */
+ protected abstract boolean isReadyForPullEnd();
+
+ /**
+ * Implemented by derived class to return whether the View is in a state
+ * where the user can Pull to Refresh by scrolling from the start.
+ *
+ * @return true if the View is currently the correct state (for example, top
+ * of a ListView)
+ */
+ protected abstract boolean isReadyForPullStart();
+
+ /**
+ * Called by {@link #onRestoreInstanceState(Parcelable)} so that derivative
+ * classes can handle their saved instance state.
+ *
+ * @param savedInstanceState - Bundle which contains saved instance state.
+ */
+ protected void onPtrRestoreInstanceState(Bundle savedInstanceState) {
+ }
+
+ /**
+ * Called by {@link #onSaveInstanceState()} so that derivative classes can
+ * save their instance state.
+ *
+ * @param saveState - Bundle to be updated with saved state.
+ */
+ protected void onPtrSaveInstanceState(Bundle saveState) {
+ }
+
+ /**
+ * Called when the UI has been to be updated to be in the
+ * {@link State#PULL_TO_REFRESH} state.
+ */
+ protected void onPullToRefresh() {
+ switch (mCurrentMode) {
+ case PULL_FROM_END:
+ mFooterLayout.pullToRefresh();
+ break;
+ case PULL_FROM_START:
+ mHeaderLayout.pullToRefresh();
+ break;
+ default:
+ // NO-OP
+ break;
+ }
+ }
+
+ /**
+ * Called when the UI has been to be updated to be in the
+ * {@link State#REFRESHING} or {@link State#MANUAL_REFRESHING} state.
+ *
+ * @param doScroll - Whether the UI should scroll for this event.
+ */
+ protected void onRefreshing(final boolean doScroll) {
+ if (mMode.showHeaderLoadingLayout()) {
+ mHeaderLayout.refreshing();
+ }
+ if (mMode.showFooterLoadingLayout()) {
+ mFooterLayout.refreshing();
+ }
+
+ if (doScroll) {
+ if (mShowViewWhileRefreshing) {
+
+ // Call Refresh Listener when the Scroll has finished
+ OnSmoothScrollFinishedListener listener = new OnSmoothScrollFinishedListener() {
+ @Override
+ public void onSmoothScrollFinished() {
+ callRefreshListener();
+ }
+ };
+
+ switch (mCurrentMode) {
+ case MANUAL_REFRESH_ONLY:
+ case PULL_FROM_END:
+ smoothScrollTo(getFooterSize(), listener);
+ break;
+ default:
+ case PULL_FROM_START:
+ smoothScrollTo(-getHeaderSize(), listener);
+ break;
+ }
+ } else {
+ smoothScrollTo(0);
+ }
+ } else {
+ // We're not scrolling, so just call Refresh Listener now
+ callRefreshListener();
+ }
+ }
+
+ /**
+ * Called when the UI has been to be updated to be in the
+ * {@link State#RELEASE_TO_REFRESH} state.
+ */
+ protected void onReleaseToRefresh() {
+ switch (mCurrentMode) {
+ case PULL_FROM_END:
+ mFooterLayout.releaseToRefresh();
+ break;
+ case PULL_FROM_START:
+ mHeaderLayout.releaseToRefresh();
+ break;
+ default:
+ // NO-OP
+ break;
+ }
+ }
+
+ /**
+ * Called when the UI has been to be updated to be in the
+ * {@link State#RESET} state.
+ */
+ protected void onReset() {
+ mIsBeingDragged = false;
+ mLayoutVisibilityChangesEnabled = true;
+
+ // Always reset both layouts, just in case...
+ mHeaderLayout.reset();
+ mFooterLayout.reset();
+
+ smoothScrollTo(0);
+ }
+
+ @Override
+ protected final void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof Bundle) {
+ Bundle bundle = (Bundle) state;
+
+ setMode(Mode.mapIntToValue(bundle.getInt(STATE_MODE, 0)));
+ mCurrentMode = Mode.mapIntToValue(bundle.getInt(STATE_CURRENT_MODE, 0));
+
+ mScrollingWhileRefreshingEnabled = bundle.getBoolean(STATE_SCROLLING_REFRESHING_ENABLED, false);
+ mShowViewWhileRefreshing = bundle.getBoolean(STATE_SHOW_REFRESHING_VIEW, true);
+
+ // Let super Restore Itself
+ super.onRestoreInstanceState(bundle.getParcelable(STATE_SUPER));
+
+ State viewState = State.mapIntToValue(bundle.getInt(STATE_STATE, 0));
+ if (viewState == State.REFRESHING || viewState == State.MANUAL_REFRESHING) {
+ setState(viewState, true);
+ }
+
+ // Now let derivative classes restore their state
+ onPtrRestoreInstanceState(bundle);
+ return;
+ }
+
+ super.onRestoreInstanceState(state);
+ }
+
+ @Override
+ protected final Parcelable onSaveInstanceState() {
+ Bundle bundle = new Bundle();
+
+ // Let derivative classes get a chance to save state first, that way we
+ // can make sure they don't overrite any of our values
+ onPtrSaveInstanceState(bundle);
+
+ bundle.putInt(STATE_STATE, mState.getIntValue());
+ bundle.putInt(STATE_MODE, mMode.getIntValue());
+ bundle.putInt(STATE_CURRENT_MODE, mCurrentMode.getIntValue());
+ bundle.putBoolean(STATE_SCROLLING_REFRESHING_ENABLED, mScrollingWhileRefreshingEnabled);
+ bundle.putBoolean(STATE_SHOW_REFRESHING_VIEW, mShowViewWhileRefreshing);
+ bundle.putParcelable(STATE_SUPER, super.onSaveInstanceState());
+
+ return bundle;
+ }
+
+ @Override
+ protected final void onSizeChanged(int w, int h, int oldw, int oldh) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, String.format("onSizeChanged. W: %d, H: %d", w, h));
+ }
+
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ // We need to update the header/footer when our size changes
+ refreshLoadingViewsSize();
+
+ // Update the Refreshable View layout
+ refreshRefreshableViewSize(w, h);
+
+ /**
+ * As we're currently in a Layout Pass, we need to schedule another one
+ * to layout any changes we've made here
+ */
+ post(new Runnable() {
+ @Override
+ public void run() {
+ requestLayout();
+ }
+ });
+ }
+
+ /**
+ * Re-measure the Loading Views height, and adjust internal padding as
+ * necessary
+ */
+ protected final void refreshLoadingViewsSize() {
+ final int maximumPullScroll = (int) (getMaximumPullScroll() * 1.2f);
+
+ int pLeft = getPaddingLeft();
+ int pTop = getPaddingTop();
+ int pRight = getPaddingRight();
+ int pBottom = getPaddingBottom();
+
+ switch (getPullToRefreshScrollDirection()) {
+ case HORIZONTAL:
+ if (mMode.showHeaderLoadingLayout()) {
+ mHeaderLayout.setWidth(maximumPullScroll);
+ pLeft = -maximumPullScroll;
+ } else {
+ pLeft = 0;
+ }
+
+ if (mMode.showFooterLoadingLayout()) {
+ mFooterLayout.setWidth(maximumPullScroll);
+ pRight = -maximumPullScroll;
+ } else {
+ pRight = 0;
+ }
+ break;
+
+ case VERTICAL:
+ if (mMode.showHeaderLoadingLayout()) {
+ mHeaderLayout.setHeight(maximumPullScroll);
+ pTop = -maximumPullScroll;
+ } else {
+ pTop = 0;
+ }
+
+ if (mMode.showFooterLoadingLayout()) {
+ mFooterLayout.setHeight(maximumPullScroll);
+ pBottom = -maximumPullScroll;
+ } else {
+ pBottom = 0;
+ }
+ break;
+ }
+
+ if (DEBUG) {
+ Log.d(LOG_TAG, String.format("Setting Padding. L: %d, T: %d, R: %d, B: %d", pLeft, pTop, pRight, pBottom));
+ }
+ setPadding(pLeft, pTop, pRight, pBottom);
+ }
+
+ protected final void refreshRefreshableViewSize(int width, int height) {
+ // We need to set the Height of the Refreshable View to the same as
+ // this layout
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mRefreshableViewWrapper.getLayoutParams();
+
+ switch (getPullToRefreshScrollDirection()) {
+ case HORIZONTAL:
+ if (lp.width != width) {
+ lp.width = width;
+ mRefreshableViewWrapper.requestLayout();
+ }
+ break;
+ case VERTICAL:
+ if (lp.height != height) {
+ lp.height = height;
+ mRefreshableViewWrapper.requestLayout();
+ }
+ break;
+ }
+ }
+
+ /**
+ * Helper method which just calls scrollTo() in the correct scrolling
+ * direction.
+ *
+ * @param value - New Scroll value
+ */
+ protected final void setHeaderScroll(int value) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "setHeaderScroll: " + value);
+ }
+
+ // Clamp value to with pull scroll range
+ final int maximumPullScroll = getMaximumPullScroll();
+ value = Math.min(maximumPullScroll, Math.max(-maximumPullScroll, value));
+
+ if (mLayoutVisibilityChangesEnabled) {
+ if (value < 0) {
+ mHeaderLayout.setVisibility(View.VISIBLE);
+ } else if (value > 0) {
+ mFooterLayout.setVisibility(View.VISIBLE);
+ } else {
+ mHeaderLayout.setVisibility(View.INVISIBLE);
+ mFooterLayout.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ if (USE_HW_LAYERS) {
+ /**
+ * Use a Hardware Layer on the Refreshable View if we've scrolled at
+ * all. We don't use them on the Header/Footer Views as they change
+ * often, which would negate any HW layer performance boost.
+ */
+ ViewCompat.setLayerType(mRefreshableViewWrapper, value != 0 ? View.LAYER_TYPE_HARDWARE
+ : View.LAYER_TYPE_NONE);
+ }
+
+ switch (getPullToRefreshScrollDirection()) {
+ case VERTICAL:
+ scrollTo(0, value);
+ break;
+ case HORIZONTAL:
+ scrollTo(value, 0);
+ break;
+ }
+ }
+
+ /**
+ * Smooth Scroll to position using the default duration of
+ * {@value #SMOOTH_SCROLL_DURATION_MS} ms.
+ *
+ * @param scrollValue - Position to scroll to
+ */
+ protected final void smoothScrollTo(int scrollValue) {
+ smoothScrollTo(scrollValue, getPullToRefreshScrollDuration());
+ }
+
+ /**
+ * Smooth Scroll to position using the default duration of
+ * {@value #SMOOTH_SCROLL_DURATION_MS} ms.
+ *
+ * @param scrollValue - Position to scroll to
+ * @param listener - Listener for scroll
+ */
+ protected final void smoothScrollTo(int scrollValue, OnSmoothScrollFinishedListener listener) {
+ smoothScrollTo(scrollValue, getPullToRefreshScrollDuration(), 0, listener);
+ }
+
+ /**
+ * Smooth Scroll to position using the longer default duration of
+ * {@value #SMOOTH_SCROLL_LONG_DURATION_MS} ms.
+ *
+ * @param scrollValue - Position to scroll to
+ */
+ protected final void smoothScrollToLonger(int scrollValue) {
+ smoothScrollTo(scrollValue, getPullToRefreshScrollDurationLonger());
+ }
+
+ /**
+ * Updates the View State when the mode has been set. This does not do any
+ * checking that the mode is different to current state so always updates.
+ */
+ protected void updateUIForMode() {
+ // We need to use the correct LayoutParam values, based on scroll
+ // direction
+ final LinearLayout.LayoutParams lp = getLoadingLayoutLayoutParams();
+
+ // Remove Header, and then add Header Loading View again if needed
+ if (this == mHeaderLayout.getParent()) {
+ removeView(mHeaderLayout);
+ }
+ if (mMode.showHeaderLoadingLayout()) {
+ addViewInternal(mHeaderLayout, 0, lp);
+ }
+
+ // Remove Footer, and then add Footer Loading View again if needed
+ if (this == mFooterLayout.getParent()) {
+ removeView(mFooterLayout);
+ }
+ if (mMode.showFooterLoadingLayout()) {
+ addViewInternal(mFooterLayout, lp);
+ }
+
+ // Hide Loading Views
+ refreshLoadingViewsSize();
+
+ // If we're not using Mode.BOTH, set mCurrentMode to mMode, otherwise
+ // set it to pull down
+ mCurrentMode = (mMode != Mode.BOTH) ? mMode : Mode.PULL_FROM_START;
+ }
+
+ private void addRefreshableView(Context context, T refreshableView) {
+ mRefreshableViewWrapper = new FrameLayout(context);
+ mRefreshableViewWrapper.addView(refreshableView, ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+
+ addViewInternal(mRefreshableViewWrapper, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT));
+ }
+
+ private void callRefreshListener() {
+ if (null != mOnRefreshListener) {
+ mOnRefreshListener.onRefresh(this);
+ } else if (null != mOnRefreshListener2) {
+ if (mCurrentMode == Mode.PULL_FROM_START) {
+ mOnRefreshListener2.onPullDownToRefresh(this);
+ } else if (mCurrentMode == Mode.PULL_FROM_END) {
+ mOnRefreshListener2.onPullUpToRefresh(this);
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void init(Context context, AttributeSet attrs) {
+ switch (getPullToRefreshScrollDirection()) {
+ case HORIZONTAL:
+ setOrientation(LinearLayout.HORIZONTAL);
+ break;
+ case VERTICAL:
+ default:
+ setOrientation(LinearLayout.VERTICAL);
+ break;
+ }
+
+ setGravity(Gravity.CENTER);
+
+ ViewConfiguration config = ViewConfiguration.get(context);
+ mTouchSlop = config.getScaledTouchSlop();
+
+ // Styleables from XML
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh);
+
+ if (a.hasValue(R.styleable.PullToRefresh_ptrMode)) {
+ mMode = Mode.mapIntToValue(a.getInteger(R.styleable.PullToRefresh_ptrMode, 0));
+ }
+
+ if (a.hasValue(R.styleable.PullToRefresh_ptrAnimationStyle)) {
+ mLoadingAnimationStyle = AnimationStyle.mapIntToValue(a.getInteger(
+ R.styleable.PullToRefresh_ptrAnimationStyle, 0));
+ }
+
+ // Refreshable View
+ // By passing the attrs, we can add ListView/GridView params via XML
+ mRefreshableView = createRefreshableView(context, attrs);
+ addRefreshableView(context, mRefreshableView);
+
+ // We need to create now layouts now
+ mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);
+ mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);
+
+ /**
+ * Styleables from XML
+ */
+ if (a.hasValue(R.styleable.PullToRefresh_ptrRefreshableViewBackground)) {
+ Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrRefreshableViewBackground);
+ if (null != background) {
+ mRefreshableView.setBackgroundDrawable(background);
+ }
+ } else if (a.hasValue(R.styleable.PullToRefresh_ptrAdapterViewBackground)) {
+ Utils.warnDeprecation("ptrAdapterViewBackground", "ptrRefreshableViewBackground");
+ Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrAdapterViewBackground);
+ if (null != background) {
+ mRefreshableView.setBackgroundDrawable(background);
+ }
+ }
+
+ if (a.hasValue(R.styleable.PullToRefresh_ptrOverScroll)) {
+ mOverScrollEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrOverScroll, true);
+ }
+
+ if (a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) {
+ mScrollingWhileRefreshingEnabled = a.getBoolean(
+ R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled, false);
+ }
+
+ // Let the derivative classes have a go at handling attributes, then
+ // recycle them...
+ handleStyledAttributes(a);
+ a.recycle();
+
+ // Finally update the UI for the modes
+ updateUIForMode();
+ }
+
+ private boolean isReadyForPull() {
+ switch (mMode) {
+ case PULL_FROM_START:
+ return isReadyForPullStart();
+ case PULL_FROM_END:
+ return isReadyForPullEnd();
+ case BOTH:
+ return isReadyForPullEnd() || isReadyForPullStart();
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Actions a Pull Event
+ *
+ * @return true if the Event has been handled, false if there has been no
+ * change
+ */
+ private void pullEvent() {
+ final int newScrollValue;
+ final int itemDimension;
+ final float initialMotionValue, lastMotionValue;
+
+ switch (getPullToRefreshScrollDirection()) {
+ case HORIZONTAL:
+ initialMotionValue = mInitialMotionX;
+ lastMotionValue = mLastMotionX;
+ break;
+ case VERTICAL:
+ default:
+ initialMotionValue = mInitialMotionY;
+ lastMotionValue = mLastMotionY;
+ break;
+ }
+
+ switch (mCurrentMode) {
+ case PULL_FROM_END:
+ newScrollValue = Math.round(Math.max(initialMotionValue - lastMotionValue, 0) / FRICTION);
+ itemDimension = getFooterSize();
+ break;
+ case PULL_FROM_START:
+ default:
+ newScrollValue = Math.round(Math.min(initialMotionValue - lastMotionValue, 0) / FRICTION);
+ itemDimension = getHeaderSize();
+ break;
+ }
+
+ setHeaderScroll(newScrollValue);
+
+ if (newScrollValue != 0 && !isRefreshing()) {
+ float scale = Math.abs(newScrollValue) / (float) itemDimension;
+ switch (mCurrentMode) {
+ case PULL_FROM_END:
+ mFooterLayout.onPull(scale);
+ break;
+ case PULL_FROM_START:
+ default:
+ mHeaderLayout.onPull(scale);
+ break;
+ }
+
+ if (mState != State.PULL_TO_REFRESH && itemDimension >= Math.abs(newScrollValue)) {
+ setState(State.PULL_TO_REFRESH);
+ } else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue)) {
+ setState(State.RELEASE_TO_REFRESH);
+ }
+ }
+ }
+
+ private LinearLayout.LayoutParams getLoadingLayoutLayoutParams() {
+ switch (getPullToRefreshScrollDirection()) {
+ case HORIZONTAL:
+ return new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.MATCH_PARENT);
+ case VERTICAL:
+ default:
+ return new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ }
+ }
+
+ private int getMaximumPullScroll() {
+ switch (getPullToRefreshScrollDirection()) {
+ case HORIZONTAL:
+ return Math.round(getWidth() / FRICTION);
+ case VERTICAL:
+ default:
+ return Math.round(getHeight() / FRICTION);
+ }
+ }
+
+ /**
+ * Smooth Scroll to position using the specific duration
+ *
+ * @param scrollValue - Position to scroll to
+ * @param duration - Duration of animation in milliseconds
+ */
+ private final void smoothScrollTo(int scrollValue, long duration) {
+ smoothScrollTo(scrollValue, duration, 0, null);
+ }
+
+ private final void smoothScrollTo(int newScrollValue, long duration, long delayMillis,
+ OnSmoothScrollFinishedListener listener) {
+ if (null != mCurrentSmoothScrollRunnable) {
+ mCurrentSmoothScrollRunnable.stop();
+ }
+
+ final int oldScrollValue;
+ switch (getPullToRefreshScrollDirection()) {
+ case HORIZONTAL:
+ oldScrollValue = getScrollX();
+ break;
+ case VERTICAL:
+ default:
+ oldScrollValue = getScrollY();
+ break;
+ }
+
+ if (oldScrollValue != newScrollValue) {
+ if (null == mScrollAnimationInterpolator) {
+ // Default interpolator is a Decelerate Interpolator
+ mScrollAnimationInterpolator = new DecelerateInterpolator();
+ }
+ mCurrentSmoothScrollRunnable = new SmoothScrollRunnable(oldScrollValue, newScrollValue, duration, listener);
+
+ if (delayMillis > 0) {
+ postDelayed(mCurrentSmoothScrollRunnable, delayMillis);
+ } else {
+ post(mCurrentSmoothScrollRunnable);
+ }
+ }
+ }
+
+ private final void smoothScrollToAndBack(int y) {
+ smoothScrollTo(y, SMOOTH_SCROLL_DURATION_MS, 0, new OnSmoothScrollFinishedListener() {
+
+ @Override
+ public void onSmoothScrollFinished() {
+ smoothScrollTo(0, SMOOTH_SCROLL_DURATION_MS, DEMO_SCROLL_INTERVAL, null);
+ }
+ });
+ }
+
+ public static enum AnimationStyle {
+ /**
+ * This is the default for Android-PullToRefresh. Allows you to use any
+ * drawable, which is automatically rotated and used as a Progress Bar.
+ */
+ ROTATE,
+
+ /**
+ * This is the old default, and what is commonly used on iOS. Uses an
+ * arrow image which flips depending on where the user has scrolled.
+ */
+ FLIP;
+
+ static AnimationStyle getDefault() {
+ return ROTATE;
+ }
+
+ /**
+ * Maps an int to a specific mode. This is needed when saving state, or
+ * inflating the view from XML where the mode is given through a attr
+ * int.
+ *
+ * @param modeInt - int to map a Mode to
+ * @return Mode that modeInt maps to, or ROTATE by default.
+ */
+ static AnimationStyle mapIntToValue(int modeInt) {
+ switch (modeInt) {
+ case 0x0:
+ default:
+ return ROTATE;
+ case 0x1:
+ return FLIP;
+ }
+ }
+
+ LoadingLayout createLoadingLayout(Context context, Mode mode, Orientation scrollDirection, TypedArray attrs) {
+ switch (this) {
+ case ROTATE:
+ default:
+ return new RotateLoadingLayout(context, mode, scrollDirection, attrs);
+ case FLIP:
+ return new FlipLoadingLayout(context, mode, scrollDirection, attrs);
+ }
+ }
+ }
+
+ public static enum Mode {
+
+ /**
+ * Disable all Pull-to-Refresh gesture and Refreshing handling
+ */
+ DISABLED(0x0),
+
+ /**
+ * Only allow the user to Pull from the start of the Refreshable View to
+ * refresh. The start is either the Top or Left, depending on the
+ * scrolling direction.
+ */
+ PULL_FROM_START(0x1),
+
+ /**
+ * Only allow the user to Pull from the end of the Refreshable View to
+ * refresh. The start is either the Bottom or Right, depending on the
+ * scrolling direction.
+ */
+ PULL_FROM_END(0x2),
+
+ /**
+ * Allow the user to both Pull from the start, from the end to refresh.
+ */
+ BOTH(0x3),
+
+ /**
+ * Disables Pull-to-Refresh gesture handling, but allows manually
+ * setting the Refresh state via
+ * {@link PullToRefreshBase#setRefreshing() setRefreshing()}.
+ */
+ MANUAL_REFRESH_ONLY(0x4);
+
+ /**
+ * @deprecated Use {@link #PULL_FROM_START} from now on.
+ */
+ public static Mode PULL_DOWN_TO_REFRESH = Mode.PULL_FROM_START;
+
+ /**
+ * @deprecated Use {@link #PULL_FROM_END} from now on.
+ */
+ public static Mode PULL_UP_TO_REFRESH = Mode.PULL_FROM_END;
+
+ /**
+ * Maps an int to a specific mode. This is needed when saving state, or
+ * inflating the view from XML where the mode is given through a attr
+ * int.
+ *
+ * @param modeInt - int to map a Mode to
+ * @return Mode that modeInt maps to, or PULL_FROM_START by default.
+ */
+ static Mode mapIntToValue(final int modeInt) {
+ for (Mode value : Mode.values()) {
+ if (modeInt == value.getIntValue()) {
+ return value;
+ }
+ }
+
+ // If not, return default
+ return getDefault();
+ }
+
+ static Mode getDefault() {
+ return PULL_FROM_START;
+ }
+
+ private int mIntValue;
+
+ // The modeInt values need to match those from attrs.xml
+ Mode(int modeInt) {
+ mIntValue = modeInt;
+ }
+
+ /**
+ * @return true if the mode permits Pull-to-Refresh
+ */
+ boolean permitsPullToRefresh() {
+ return !(this == DISABLED || this == MANUAL_REFRESH_ONLY);
+ }
+
+ /**
+ * @return true if this mode wants the Loading Layout Header to be shown
+ */
+ public boolean showHeaderLoadingLayout() {
+ return this == PULL_FROM_START || this == BOTH;
+ }
+
+ /**
+ * @return true if this mode wants the Loading Layout Footer to be shown
+ */
+ public boolean showFooterLoadingLayout() {
+ return this == PULL_FROM_END || this == BOTH || this == MANUAL_REFRESH_ONLY;
+ }
+
+ int getIntValue() {
+ return mIntValue;
+ }
+
+ }
+
+ // ===========================================================
+ // Inner, Anonymous Classes, and Enumerations
+ // ===========================================================
+
+ /**
+ * Simple Listener that allows you to be notified when the user has scrolled
+ * to the end of the AdapterView. See (
+ * {@link PullToRefreshAdapterViewBase#setOnLastItemVisibleListener}.
+ *
+ * @author Chris Banes
+ */
+ public static interface OnLastItemVisibleListener {
+
+ /**
+ * Called when the user has scrolled to the end of the list
+ */
+ public void onLastItemVisible();
+
+ }
+
+ /**
+ * Listener that allows you to be notified when the user has started or
+ * finished a touch event. Useful when you want to append extra UI events
+ * (such as sounds). See (
+ * {@link PullToRefreshAdapterViewBase#setOnPullEventListener}.
+ *
+ * @author Chris Banes
+ */
+ public static interface OnPullEventListener {
+
+ /**
+ * Called when the internal state has been changed, usually by the user
+ * pulling.
+ *
+ * @param refreshView - View which has had it's state change.
+ * @param state - The new state of View.
+ * @param direction - One of {@link Mode#PULL_FROM_START} or
+ * {@link Mode#PULL_FROM_END} depending on which direction
+ * the user is pulling. Only useful when state is
+ * {@link State#PULL_TO_REFRESH} or
+ * {@link State#RELEASE_TO_REFRESH}.
+ */
+ public void onPullEvent(final PullToRefreshBase refreshView, State state, Mode direction);
+
+ }
+
+ /**
+ * Simple Listener to listen for any callbacks to Refresh.
+ *
+ * @author Chris Banes
+ */
+ public static interface OnRefreshListener {
+
+ /**
+ * onRefresh will be called for both a Pull from start, and Pull from
+ * end
+ */
+ public void onRefresh(final PullToRefreshBase refreshView);
+
+ }
+
+ /**
+ * An advanced version of the Listener to listen for callbacks to Refresh.
+ * This listener is different as it allows you to differentiate between Pull
+ * Ups, and Pull Downs.
+ *
+ * @author Chris Banes
+ */
+ public static interface OnRefreshListener2 {
+ // TODO These methods need renaming to START/END rather than DOWN/UP
+
+ /**
+ * onPullDownToRefresh will be called only when the user has Pulled from
+ * the start, and released.
+ */
+ public void onPullDownToRefresh(final PullToRefreshBase refreshView);
+
+ /**
+ * onPullUpToRefresh will be called only when the user has Pulled from
+ * the end, and released.
+ */
+ public void onPullUpToRefresh(final PullToRefreshBase refreshView);
+
+ }
+
+ public static enum Orientation {
+ VERTICAL, HORIZONTAL;
+ }
+
+ public static enum State {
+
+ /**
+ * When the UI is in a state which means that user is not interacting
+ * with the Pull-to-Refresh function.
+ */
+ RESET(0x0),
+
+ /**
+ * When the UI is being pulled by the user, but has not been pulled far
+ * enough so that it refreshes when released.
+ */
+ PULL_TO_REFRESH(0x1),
+
+ /**
+ * When the UI is being pulled by the user, and has
+ * been pulled far enough so that it will refresh when released.
+ */
+ RELEASE_TO_REFRESH(0x2),
+
+ /**
+ * When the UI is currently refreshing, caused by a pull gesture.
+ */
+ REFRESHING(0x8),
+
+ /**
+ * When the UI is currently refreshing, caused by a call to
+ * {@link PullToRefreshBase#setRefreshing() setRefreshing()}.
+ */
+ MANUAL_REFRESHING(0x9),
+
+ /**
+ * When the UI is currently overscrolling, caused by a fling on the
+ * Refreshable View.
+ */
+ OVERSCROLLING(0x10);
+
+ /**
+ * Maps an int to a specific state. This is needed when saving state.
+ *
+ * @param stateInt - int to map a State to
+ * @return State that stateInt maps to
+ */
+ static State mapIntToValue(final int stateInt) {
+ for (State value : State.values()) {
+ if (stateInt == value.getIntValue()) {
+ return value;
+ }
+ }
+
+ // If not, return default
+ return RESET;
+ }
+
+ private int mIntValue;
+
+ State(int intValue) {
+ mIntValue = intValue;
+ }
+
+ int getIntValue() {
+ return mIntValue;
+ }
+ }
+
+ final class SmoothScrollRunnable implements Runnable {
+ private final Interpolator mInterpolator;
+ private final int mScrollToY;
+ private final int mScrollFromY;
+ private final long mDuration;
+ private OnSmoothScrollFinishedListener mListener;
+
+ private boolean mContinueRunning = true;
+ private long mStartTime = -1;
+ private int mCurrentY = -1;
+
+ public SmoothScrollRunnable(int fromY, int toY, long duration, OnSmoothScrollFinishedListener listener) {
+ mScrollFromY = fromY;
+ mScrollToY = toY;
+ mInterpolator = mScrollAnimationInterpolator;
+ mDuration = duration;
+ mListener = listener;
+ }
+
+ @Override
+ public void run() {
+
+ /**
+ * Only set mStartTime if this is the first time we're starting,
+ * else actually calculate the Y delta
+ */
+ if (mStartTime == -1) {
+ mStartTime = System.currentTimeMillis();
+ } else {
+
+ /**
+ * We do do all calculations in long to reduce software float
+ * calculations. We use 1000 as it gives us good accuracy and
+ * small rounding errors
+ */
+ long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime)) / mDuration;
+ normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);
+
+ final int deltaY = Math.round((mScrollFromY - mScrollToY)
+ * mInterpolator.getInterpolation(normalizedTime / 1000f));
+ mCurrentY = mScrollFromY - deltaY;
+ setHeaderScroll(mCurrentY);
+ }
+
+ // If we're not at the target Y, keep going...
+ if (mContinueRunning && mScrollToY != mCurrentY) {
+ ViewCompat.postOnAnimation(PullToRefreshBase.this, this);
+ } else {
+ if (null != mListener) {
+ mListener.onSmoothScrollFinished();
+ }
+ }
+ }
+
+ public void stop() {
+ mContinueRunning = false;
+ removeCallbacks(this);
+ }
+ }
+
+ static interface OnSmoothScrollFinishedListener {
+ void onSmoothScrollFinished();
+ }
+
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshExpandableListView.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshExpandableListView.java
new file mode 100644
index 00000000..649020c6
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshExpandableListView.java
@@ -0,0 +1,103 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ExpandableListView;
+
+import com.handmark.pulltorefresh.library.internal.EmptyViewMethodAccessor;
+
+public class PullToRefreshExpandableListView extends PullToRefreshAdapterViewBase {
+
+ public PullToRefreshExpandableListView(Context context) {
+ super(context);
+ }
+
+ public PullToRefreshExpandableListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public PullToRefreshExpandableListView(Context context, Mode mode) {
+ super(context, mode);
+ }
+
+ public PullToRefreshExpandableListView(Context context, Mode mode, AnimationStyle style) {
+ super(context, mode, style);
+ }
+
+ @Override
+ public final Orientation getPullToRefreshScrollDirection() {
+ return Orientation.VERTICAL;
+ }
+
+ @Override
+ protected ExpandableListView createRefreshableView(Context context, AttributeSet attrs) {
+ final ExpandableListView lv;
+ if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
+ lv = new InternalExpandableListViewSDK9(context, attrs);
+ } else {
+ lv = new InternalExpandableListView(context, attrs);
+ }
+
+ // Set it to this so it can be used in ListActivity/ListFragment
+ lv.setId(android.R.id.list);
+ return lv;
+ }
+
+ class InternalExpandableListView extends ExpandableListView implements EmptyViewMethodAccessor {
+
+ public InternalExpandableListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void setEmptyView(View emptyView) {
+ PullToRefreshExpandableListView.this.setEmptyView(emptyView);
+ }
+
+ @Override
+ public void setEmptyViewInternal(View emptyView) {
+ super.setEmptyView(emptyView);
+ }
+ }
+
+ @TargetApi(9)
+ final class InternalExpandableListViewSDK9 extends InternalExpandableListView {
+
+ public InternalExpandableListViewSDK9(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX,
+ int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
+
+ final boolean returnValue = super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
+ scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
+
+ // Does all of the hard work...
+ OverscrollHelper.overScrollBy(PullToRefreshExpandableListView.this, deltaX, scrollX, deltaY, scrollY,
+ isTouchEvent);
+
+ return returnValue;
+ }
+ }
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshGridView.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshGridView.java
new file mode 100644
index 00000000..f9506889
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshGridView.java
@@ -0,0 +1,102 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.GridView;
+
+import com.handmark.pulltorefresh.library.internal.EmptyViewMethodAccessor;
+
+public class PullToRefreshGridView extends PullToRefreshAdapterViewBase {
+
+ public PullToRefreshGridView(Context context) {
+ super(context);
+ }
+
+ public PullToRefreshGridView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public PullToRefreshGridView(Context context, Mode mode) {
+ super(context, mode);
+ }
+
+ public PullToRefreshGridView(Context context, Mode mode, AnimationStyle style) {
+ super(context, mode, style);
+ }
+
+ @Override
+ public final Orientation getPullToRefreshScrollDirection() {
+ return Orientation.VERTICAL;
+ }
+
+ @Override
+ protected final GridView createRefreshableView(Context context, AttributeSet attrs) {
+ final GridView gv;
+ if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
+ gv = new InternalGridViewSDK9(context, attrs);
+ } else {
+ gv = new InternalGridView(context, attrs);
+ }
+
+ // Use Generated ID (from res/values/ids.xml)
+ gv.setId(R.id.gridview);
+ return gv;
+ }
+
+ class InternalGridView extends GridView implements EmptyViewMethodAccessor {
+
+ public InternalGridView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void setEmptyView(View emptyView) {
+ PullToRefreshGridView.this.setEmptyView(emptyView);
+ }
+
+ @Override
+ public void setEmptyViewInternal(View emptyView) {
+ super.setEmptyView(emptyView);
+ }
+ }
+
+ @TargetApi(9)
+ final class InternalGridViewSDK9 extends InternalGridView {
+
+ public InternalGridViewSDK9(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX,
+ int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
+
+ final boolean returnValue = super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
+ scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
+
+ // Does all of the hard work...
+ OverscrollHelper.overScrollBy(PullToRefreshGridView.this, deltaX, scrollX, deltaY, scrollY, isTouchEvent);
+
+ return returnValue;
+ }
+ }
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshHorizontalScrollView.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshHorizontalScrollView.java
new file mode 100644
index 00000000..a70f7ad2
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshHorizontalScrollView.java
@@ -0,0 +1,110 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.HorizontalScrollView;
+
+public class PullToRefreshHorizontalScrollView extends PullToRefreshBase {
+
+ public PullToRefreshHorizontalScrollView(Context context) {
+ super(context);
+ }
+
+ public PullToRefreshHorizontalScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public PullToRefreshHorizontalScrollView(Context context, Mode mode) {
+ super(context, mode);
+ }
+
+ public PullToRefreshHorizontalScrollView(Context context, Mode mode, AnimationStyle style) {
+ super(context, mode, style);
+ }
+
+ @Override
+ public final Orientation getPullToRefreshScrollDirection() {
+ return Orientation.HORIZONTAL;
+ }
+
+ @Override
+ protected HorizontalScrollView createRefreshableView(Context context, AttributeSet attrs) {
+ HorizontalScrollView scrollView;
+
+ if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
+ scrollView = new InternalHorizontalScrollViewSDK9(context, attrs);
+ } else {
+ scrollView = new HorizontalScrollView(context, attrs);
+ }
+
+ scrollView.setId(R.id.scrollview);
+ return scrollView;
+ }
+
+ @Override
+ protected boolean isReadyForPullStart() {
+ return mRefreshableView.getScrollX() == 0;
+ }
+
+ @Override
+ protected boolean isReadyForPullEnd() {
+ View scrollViewChild = mRefreshableView.getChildAt(0);
+ if (null != scrollViewChild) {
+ return mRefreshableView.getScrollX() >= (scrollViewChild.getWidth() - getWidth());
+ }
+ return false;
+ }
+
+ @TargetApi(9)
+ final class InternalHorizontalScrollViewSDK9 extends HorizontalScrollView {
+
+ public InternalHorizontalScrollViewSDK9(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX,
+ int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
+
+ final boolean returnValue = super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
+ scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
+
+ // Does all of the hard work...
+ OverscrollHelper.overScrollBy(PullToRefreshHorizontalScrollView.this, deltaX, scrollX, deltaY, scrollY,
+ getScrollRange(), isTouchEvent);
+
+ return returnValue;
+ }
+
+ /**
+ * Taken from the AOSP ScrollView source
+ */
+ private int getScrollRange() {
+ int scrollRange = 0;
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ scrollRange = Math.max(0, child.getWidth() - (getWidth() - getPaddingLeft() - getPaddingRight()));
+ }
+ return scrollRange;
+ }
+ }
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshListView.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshListView.java
new file mode 100644
index 00000000..0aa9a27d
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshListView.java
@@ -0,0 +1,337 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+import com.handmark.pulltorefresh.library.internal.EmptyViewMethodAccessor;
+import com.handmark.pulltorefresh.library.internal.LoadingLayout;
+
+public class PullToRefreshListView extends PullToRefreshAdapterViewBase {
+
+ private LoadingLayout mHeaderLoadingView;
+ private LoadingLayout mFooterLoadingView;
+
+ private FrameLayout mLvFooterLoadingFrame;
+
+ private boolean mListViewExtrasEnabled;
+
+ public PullToRefreshListView(Context context) {
+ super(context);
+ }
+
+ public PullToRefreshListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public PullToRefreshListView(Context context, Mode mode) {
+ super(context, mode);
+ }
+
+ public PullToRefreshListView(Context context, Mode mode, AnimationStyle style) {
+ super(context, mode, style);
+ }
+
+ @Override
+ public final Orientation getPullToRefreshScrollDirection() {
+ return Orientation.VERTICAL;
+ }
+
+ @Override
+ protected void onRefreshing(final boolean doScroll) {
+ /**
+ * If we're not showing the Refreshing view, or the list is empty, the
+ * the header/footer views won't show so we use the normal method.
+ */
+ ListAdapter adapter = mRefreshableView.getAdapter();
+ if (!mListViewExtrasEnabled || !getShowViewWhileRefreshing() || null == adapter || adapter.isEmpty()) {
+ super.onRefreshing(doScroll);
+ return;
+ }
+
+ super.onRefreshing(false);
+
+ final LoadingLayout origLoadingView, listViewLoadingView, oppositeListViewLoadingView;
+ final int selection, scrollToY;
+
+ switch (getCurrentMode()) {
+ case MANUAL_REFRESH_ONLY:
+ case PULL_FROM_END:
+ origLoadingView = getFooterLayout();
+ listViewLoadingView = mFooterLoadingView;
+ oppositeListViewLoadingView = mHeaderLoadingView;
+ selection = mRefreshableView.getCount() - 1;
+ scrollToY = getScrollY() - getFooterSize();
+ break;
+ case PULL_FROM_START:
+ default:
+ origLoadingView = getHeaderLayout();
+ listViewLoadingView = mHeaderLoadingView;
+ oppositeListViewLoadingView = mFooterLoadingView;
+ selection = 0;
+ scrollToY = getScrollY() + getHeaderSize();
+ break;
+ }
+
+ // Hide our original Loading View
+ origLoadingView.reset();
+ origLoadingView.hideAllViews();
+
+ // Make sure the opposite end is hidden too
+ oppositeListViewLoadingView.setVisibility(View.GONE);
+
+ // Show the ListView Loading View and set it to refresh.
+ listViewLoadingView.setVisibility(View.VISIBLE);
+ listViewLoadingView.refreshing();
+
+ if (doScroll) {
+ // We need to disable the automatic visibility changes for now
+ disableLoadingLayoutVisibilityChanges();
+
+ // We scroll slightly so that the ListView's header/footer is at the
+ // same Y position as our normal header/footer
+ setHeaderScroll(scrollToY);
+
+ // Make sure the ListView is scrolled to show the loading
+ // header/footer
+ mRefreshableView.setSelection(selection);
+
+ // Smooth scroll as normal
+ smoothScrollTo(0);
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ /**
+ * If the extras are not enabled, just call up to super and return.
+ */
+ if (!mListViewExtrasEnabled) {
+ super.onReset();
+ return;
+ }
+
+ final LoadingLayout originalLoadingLayout, listViewLoadingLayout;
+ final int scrollToHeight, selection;
+ final boolean scrollLvToEdge;
+
+ switch (getCurrentMode()) {
+ case MANUAL_REFRESH_ONLY:
+ case PULL_FROM_END:
+ originalLoadingLayout = getFooterLayout();
+ listViewLoadingLayout = mFooterLoadingView;
+ selection = mRefreshableView.getCount() - 1;
+ scrollToHeight = getFooterSize();
+ scrollLvToEdge = Math.abs(mRefreshableView.getLastVisiblePosition() - selection) <= 1;
+ break;
+ case PULL_FROM_START:
+ default:
+ originalLoadingLayout = getHeaderLayout();
+ listViewLoadingLayout = mHeaderLoadingView;
+ scrollToHeight = -getHeaderSize();
+ selection = 0;
+ scrollLvToEdge = Math.abs(mRefreshableView.getFirstVisiblePosition() - selection) <= 1;
+ break;
+ }
+
+ // If the ListView header loading layout is showing, then we need to
+ // flip so that the original one is showing instead
+ if (listViewLoadingLayout.getVisibility() == View.VISIBLE) {
+
+ // Set our Original View to Visible
+ originalLoadingLayout.showInvisibleViews();
+
+ // Hide the ListView Header/Footer
+ listViewLoadingLayout.setVisibility(View.GONE);
+
+ /**
+ * Scroll so the View is at the same Y as the ListView
+ * header/footer, but only scroll if: we've pulled to refresh, it's
+ * positioned correctly
+ */
+ if (scrollLvToEdge && getState() != State.MANUAL_REFRESHING) {
+ mRefreshableView.setSelection(selection);
+ setHeaderScroll(scrollToHeight);
+ }
+ }
+
+ // Finally, call up to super
+ super.onReset();
+ }
+
+ @Override
+ protected LoadingLayoutProxy createLoadingLayoutProxy(final boolean includeStart, final boolean includeEnd) {
+ LoadingLayoutProxy proxy = super.createLoadingLayoutProxy(includeStart, includeEnd);
+
+ if (mListViewExtrasEnabled) {
+ final Mode mode = getMode();
+
+ if (includeStart && mode.showHeaderLoadingLayout()) {
+ proxy.addLayout(mHeaderLoadingView);
+ }
+ if (includeEnd && mode.showFooterLoadingLayout()) {
+ proxy.addLayout(mFooterLoadingView);
+ }
+ }
+
+ return proxy;
+ }
+
+ protected ListView createListView(Context context, AttributeSet attrs) {
+ final ListView lv;
+ if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
+ lv = new InternalListViewSDK9(context, attrs);
+ } else {
+ lv = new InternalListView(context, attrs);
+ }
+ return lv;
+ }
+
+ @Override
+ protected ListView createRefreshableView(Context context, AttributeSet attrs) {
+ ListView lv = createListView(context, attrs);
+
+ // Set it to this so it can be used in ListActivity/ListFragment
+ lv.setId(android.R.id.list);
+ return lv;
+ }
+
+ @Override
+ protected void handleStyledAttributes(TypedArray a) {
+ super.handleStyledAttributes(a);
+
+ mListViewExtrasEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrListViewExtrasEnabled, true);
+
+ if (mListViewExtrasEnabled) {
+ final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);
+
+ // Create Loading Views ready for use later
+ FrameLayout frame = new FrameLayout(getContext());
+ mHeaderLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_START, a);
+ mHeaderLoadingView.setVisibility(View.GONE);
+ frame.addView(mHeaderLoadingView, lp);
+ mRefreshableView.addHeaderView(frame, null, false);
+
+ mLvFooterLoadingFrame = new FrameLayout(getContext());
+ mFooterLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_END, a);
+ mFooterLoadingView.setVisibility(View.GONE);
+ mLvFooterLoadingFrame.addView(mFooterLoadingView, lp);
+
+ /**
+ * If the value for Scrolling While Refreshing hasn't been
+ * explicitly set via XML, enable Scrolling While Refreshing.
+ */
+ if (!a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) {
+ setScrollingWhileRefreshingEnabled(true);
+ }
+ }
+ }
+
+ @TargetApi(9)
+ final class InternalListViewSDK9 extends InternalListView {
+
+ public InternalListViewSDK9(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX,
+ int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
+
+ final boolean returnValue = super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
+ scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
+
+ // Does all of the hard work...
+ OverscrollHelper.overScrollBy(PullToRefreshListView.this, deltaX, scrollX, deltaY, scrollY, isTouchEvent);
+
+ return returnValue;
+ }
+ }
+
+ protected class InternalListView extends ListView implements EmptyViewMethodAccessor {
+
+ private boolean mAddedLvFooter = false;
+
+ public InternalListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ /**
+ * This is a bit hacky, but Samsung's ListView has got a bug in it
+ * when using Header/Footer Views and the list is empty. This masks
+ * the issue so that it doesn't cause an FC. See Issue #66.
+ */
+ try {
+ super.dispatchDraw(canvas);
+ } catch (IndexOutOfBoundsException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ /**
+ * This is a bit hacky, but Samsung's ListView has got a bug in it
+ * when using Header/Footer Views and the list is empty. This masks
+ * the issue so that it doesn't cause an FC. See Issue #66.
+ */
+ try {
+ return super.dispatchTouchEvent(ev);
+ } catch (IndexOutOfBoundsException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ // Add the Footer View at the last possible moment
+ if (null != mLvFooterLoadingFrame && !mAddedLvFooter) {
+ addFooterView(mLvFooterLoadingFrame, null, false);
+ mAddedLvFooter = true;
+ }
+
+ super.setAdapter(adapter);
+ }
+
+ @Override
+ public void setEmptyView(View emptyView) {
+ PullToRefreshListView.this.setEmptyView(emptyView);
+ }
+
+ @Override
+ public void setEmptyViewInternal(View emptyView) {
+ super.setEmptyView(emptyView);
+ }
+
+ }
+
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshScrollView.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshScrollView.java
new file mode 100644
index 00000000..3ae3627f
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshScrollView.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ScrollView;
+
+public class PullToRefreshScrollView extends PullToRefreshBase {
+
+ public PullToRefreshScrollView(Context context) {
+ super(context);
+ }
+
+ public PullToRefreshScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public PullToRefreshScrollView(Context context, Mode mode) {
+ super(context, mode);
+ }
+
+ public PullToRefreshScrollView(Context context, Mode mode, AnimationStyle style) {
+ super(context, mode, style);
+ }
+
+ @Override
+ public final Orientation getPullToRefreshScrollDirection() {
+ return Orientation.VERTICAL;
+ }
+
+ @Override
+ protected ScrollView createRefreshableView(Context context, AttributeSet attrs) {
+ ScrollView scrollView;
+ if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
+ scrollView = new InternalScrollViewSDK9(context, attrs);
+ } else {
+ scrollView = new ScrollView(context, attrs);
+ }
+
+ scrollView.setId(R.id.scrollview);
+ return scrollView;
+ }
+
+ @Override
+ protected boolean isReadyForPullStart() {
+ return mRefreshableView.getScrollY() == 0;
+ }
+
+ @Override
+ protected boolean isReadyForPullEnd() {
+ View scrollViewChild = mRefreshableView.getChildAt(0);
+ if (null != scrollViewChild) {
+ return mRefreshableView.getScrollY() >= (scrollViewChild.getHeight() - getHeight());
+ }
+ return false;
+ }
+
+ @TargetApi(9)
+ final class InternalScrollViewSDK9 extends ScrollView {
+
+ public InternalScrollViewSDK9(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX,
+ int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
+
+ final boolean returnValue = super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
+ scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
+
+ // Does all of the hard work...
+ OverscrollHelper.overScrollBy(PullToRefreshScrollView.this, deltaX, scrollX, deltaY, scrollY,
+ getScrollRange(), isTouchEvent);
+
+ return returnValue;
+ }
+
+ /**
+ * Taken from the AOSP ScrollView source
+ */
+ private int getScrollRange() {
+ int scrollRange = 0;
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ scrollRange = Math.max(0, child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop()));
+ }
+ return scrollRange;
+ }
+ }
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshWebView.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshWebView.java
new file mode 100644
index 00000000..3f873de0
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshWebView.java
@@ -0,0 +1,165 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.FloatMath;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+
+public class PullToRefreshWebView extends PullToRefreshBase {
+
+ private static final OnRefreshListener defaultOnRefreshListener = new OnRefreshListener() {
+
+ @Override
+ public void onRefresh(PullToRefreshBase refreshView) {
+ refreshView.getRefreshableView().reload();
+ }
+
+ };
+
+ private final WebChromeClient defaultWebChromeClient = new WebChromeClient() {
+
+ @Override
+ public void onProgressChanged(WebView view, int newProgress) {
+ if (newProgress == 100) {
+ onRefreshComplete();
+ }
+ }
+
+ };
+
+ public PullToRefreshWebView(Context context) {
+ super(context);
+
+ /**
+ * Added so that by default, Pull-to-Refresh refreshes the page
+ */
+ setOnRefreshListener(defaultOnRefreshListener);
+ mRefreshableView.setWebChromeClient(defaultWebChromeClient);
+ }
+
+ public PullToRefreshWebView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ /**
+ * Added so that by default, Pull-to-Refresh refreshes the page
+ */
+ setOnRefreshListener(defaultOnRefreshListener);
+ mRefreshableView.setWebChromeClient(defaultWebChromeClient);
+ }
+
+ public PullToRefreshWebView(Context context, Mode mode) {
+ super(context, mode);
+
+ /**
+ * Added so that by default, Pull-to-Refresh refreshes the page
+ */
+ setOnRefreshListener(defaultOnRefreshListener);
+ mRefreshableView.setWebChromeClient(defaultWebChromeClient);
+ }
+
+ public PullToRefreshWebView(Context context, Mode mode, AnimationStyle style) {
+ super(context, mode, style);
+
+ /**
+ * Added so that by default, Pull-to-Refresh refreshes the page
+ */
+ setOnRefreshListener(defaultOnRefreshListener);
+ mRefreshableView.setWebChromeClient(defaultWebChromeClient);
+ }
+
+ @Override
+ public final Orientation getPullToRefreshScrollDirection() {
+ return Orientation.VERTICAL;
+ }
+
+ @Override
+ protected WebView createRefreshableView(Context context, AttributeSet attrs) {
+ WebView webView;
+ if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
+ webView = new InternalWebViewSDK9(context, attrs);
+ } else {
+ webView = new WebView(context, attrs);
+ }
+
+ webView.setId(R.id.webview);
+ return webView;
+ }
+
+ @Override
+ protected boolean isReadyForPullStart() {
+ return mRefreshableView.getScrollY() == 0;
+ }
+
+ @Override
+ protected boolean isReadyForPullEnd() {
+ float exactContentHeight = FloatMath.floor(mRefreshableView.getContentHeight() * mRefreshableView.getScale());
+ return mRefreshableView.getScrollY() >= (exactContentHeight - mRefreshableView.getHeight());
+ }
+
+ @Override
+ protected void onPtrRestoreInstanceState(Bundle savedInstanceState) {
+ super.onPtrRestoreInstanceState(savedInstanceState);
+ mRefreshableView.restoreState(savedInstanceState);
+ }
+
+ @Override
+ protected void onPtrSaveInstanceState(Bundle saveState) {
+ super.onPtrSaveInstanceState(saveState);
+ mRefreshableView.saveState(saveState);
+ }
+
+ @TargetApi(9)
+ final class InternalWebViewSDK9 extends WebView {
+
+ // WebView doesn't always scroll back to it's edge so we add some
+ // fuzziness
+ static final int OVERSCROLL_FUZZY_THRESHOLD = 2;
+
+ // WebView seems quite reluctant to overscroll so we use the scale
+ // factor to scale it's value
+ static final float OVERSCROLL_SCALE_FACTOR = 1.5f;
+
+ public InternalWebViewSDK9(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX,
+ int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
+
+ final boolean returnValue = super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
+ scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
+
+ // Does all of the hard work...
+ OverscrollHelper.overScrollBy(PullToRefreshWebView.this, deltaX, scrollX, deltaY, scrollY,
+ getScrollRange(), OVERSCROLL_FUZZY_THRESHOLD, OVERSCROLL_SCALE_FACTOR, isTouchEvent);
+
+ return returnValue;
+ }
+
+ private int getScrollRange() {
+ return (int) Math.max(0, FloatMath.floor(mRefreshableView.getContentHeight() * mRefreshableView.getScale())
+ - (getHeight() - getPaddingBottom() - getPaddingTop()));
+ }
+ }
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/extras/PullToRefreshWebView2.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/extras/PullToRefreshWebView2.java
new file mode 100644
index 00000000..78192037
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/extras/PullToRefreshWebView2.java
@@ -0,0 +1,132 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library.extras;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.webkit.WebView;
+
+import com.handmark.pulltorefresh.library.PullToRefreshWebView;
+
+/**
+ * An advanced version of {@link PullToRefreshWebView} which delegates the
+ * triggering of the PullToRefresh gesture to the Javascript running within the
+ * WebView. This means that you should only use this class if:
+ *
+ *
+ *
{@link PullToRefreshWebView} doesn't work correctly because you're using
+ * overflow:scroll or something else which means
+ * {@link WebView#getScrollY()} doesn't return correct values.
+ *
You control the web content being displayed, as you need to write some
+ * Javascript callbacks.
+ *
+ *
+ *
+ * The way this call works is that when a PullToRefresh gesture is in action,
+ * the following Javascript methods will be called:
+ * isReadyForPullDown() and isReadyForPullUp(), it is
+ * your job to calculate whether the view is in a state where a PullToRefresh
+ * can happen, and return the result via the callback mechanism. An example can
+ * be seen below:
+ *
+ *
+ *
+ * function isReadyForPullDown() {
+ * var result = ... // Probably using the .scrollTop DOM attribute
+ * ptr.isReadyForPullDownResponse(result);
+ * }
+ *
+ * function isReadyForPullUp() {
+ * var result = ... // Probably using the .scrollBottom DOM attribute
+ * ptr.isReadyForPullUpResponse(result);
+ * }
+ *
+ *
+ * @author Chris Banes
+ */
+public class PullToRefreshWebView2 extends PullToRefreshWebView {
+
+ static final String JS_INTERFACE_PKG = "ptr";
+ static final String DEF_JS_READY_PULL_DOWN_CALL = "javascript:isReadyForPullDown();";
+ static final String DEF_JS_READY_PULL_UP_CALL = "javascript:isReadyForPullUp();";
+
+ public PullToRefreshWebView2(Context context) {
+ super(context);
+ }
+
+ public PullToRefreshWebView2(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public PullToRefreshWebView2(Context context, Mode mode) {
+ super(context, mode);
+ }
+
+ private JsValueCallback mJsCallback;
+ private final AtomicBoolean mIsReadyForPullDown = new AtomicBoolean(false);
+ private final AtomicBoolean mIsReadyForPullUp = new AtomicBoolean(false);
+
+ @Override
+ protected WebView createRefreshableView(Context context, AttributeSet attrs) {
+ WebView webView = super.createRefreshableView(context, attrs);
+
+ // Need to add JS Interface so we can get the response back
+ mJsCallback = new JsValueCallback();
+ webView.addJavascriptInterface(mJsCallback, JS_INTERFACE_PKG);
+
+ return webView;
+ }
+
+ @Override
+ protected boolean isReadyForPullStart() {
+ // Call Javascript...
+ getRefreshableView().loadUrl(DEF_JS_READY_PULL_DOWN_CALL);
+
+ // Response will be given to JsValueCallback, which will update
+ // mIsReadyForPullDown
+
+ return mIsReadyForPullDown.get();
+ }
+
+ @Override
+ protected boolean isReadyForPullEnd() {
+ // Call Javascript...
+ getRefreshableView().loadUrl(DEF_JS_READY_PULL_UP_CALL);
+
+ // Response will be given to JsValueCallback, which will update
+ // mIsReadyForPullUp
+
+ return mIsReadyForPullUp.get();
+ }
+
+ /**
+ * Used for response from Javascript
+ *
+ * @author Chris Banes
+ */
+ final class JsValueCallback {
+
+ public void isReadyForPullUpResponse(boolean response) {
+ mIsReadyForPullUp.set(response);
+ }
+
+ public void isReadyForPullDownResponse(boolean response) {
+ mIsReadyForPullDown.set(response);
+ }
+ }
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/extras/SoundPullEventListener.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/extras/SoundPullEventListener.java
new file mode 100644
index 00000000..a7aac306
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/extras/SoundPullEventListener.java
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library.extras;
+
+import java.util.HashMap;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.view.View;
+
+import com.handmark.pulltorefresh.library.PullToRefreshBase;
+import com.handmark.pulltorefresh.library.PullToRefreshBase.Mode;
+import com.handmark.pulltorefresh.library.PullToRefreshBase.State;
+
+public class SoundPullEventListener implements PullToRefreshBase.OnPullEventListener {
+
+ private final Context mContext;
+ private final HashMap mSoundMap;
+
+ private MediaPlayer mCurrentMediaPlayer;
+
+ /**
+ * Constructor
+ *
+ * @param context - Context
+ */
+ public SoundPullEventListener(Context context) {
+ mContext = context;
+ mSoundMap = new HashMap();
+ }
+
+ @Override
+ public final void onPullEvent(PullToRefreshBase refreshView, State event, Mode direction) {
+ Integer soundResIdObj = mSoundMap.get(event);
+ if (null != soundResIdObj) {
+ playSound(soundResIdObj.intValue());
+ }
+ }
+
+ /**
+ * Set the Sounds to be played when a Pull Event happens. You specify which
+ * sound plays for which events by calling this method multiple times for
+ * each event.
+ *
+ * If you've already set a sound for a certain event, and add another sound
+ * for that event, only the new sound will be played.
+ *
+ * @param event - The event for which the sound will be played.
+ * @param resId - Resource Id of the sound file to be played (e.g.
+ * R.raw.pull_sound)
+ */
+ public void addSoundEvent(State event, int resId) {
+ mSoundMap.put(event, resId);
+ }
+
+ /**
+ * Clears all of the previously set sounds and events.
+ */
+ public void clearSounds() {
+ mSoundMap.clear();
+ }
+
+ /**
+ * Gets the current (or last) MediaPlayer instance.
+ */
+ public MediaPlayer getCurrentMediaPlayer() {
+ return mCurrentMediaPlayer;
+ }
+
+ private void playSound(int resId) {
+ // Stop current player, if there's one playing
+ if (null != mCurrentMediaPlayer) {
+ mCurrentMediaPlayer.stop();
+ mCurrentMediaPlayer.release();
+ }
+
+ mCurrentMediaPlayer = MediaPlayer.create(mContext, resId);
+ if (null != mCurrentMediaPlayer) {
+ mCurrentMediaPlayer.start();
+ }
+ }
+
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/EmptyViewMethodAccessor.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/EmptyViewMethodAccessor.java
new file mode 100644
index 00000000..369f21e8
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/EmptyViewMethodAccessor.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library.internal;
+
+import android.view.View;
+
+/**
+ * Interface that allows PullToRefreshBase to hijack the call to
+ * AdapterView.setEmptyView()
+ *
+ * @author chris
+ */
+public interface EmptyViewMethodAccessor {
+
+ /**
+ * Calls upto AdapterView.setEmptyView()
+ *
+ * @param emptyView - to set as Empty View
+ */
+ public void setEmptyViewInternal(View emptyView);
+
+ /**
+ * Should call PullToRefreshBase.setEmptyView() which will then
+ * automatically call through to setEmptyViewInternal()
+ *
+ * @param emptyView - to set as Empty View
+ */
+ public void setEmptyView(View emptyView);
+
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/FlipLoadingLayout.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/FlipLoadingLayout.java
new file mode 100644
index 00000000..fef31605
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/FlipLoadingLayout.java
@@ -0,0 +1,146 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library.internal;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.RotateAnimation;
+import android.widget.ImageView.ScaleType;
+
+import com.handmark.pulltorefresh.library.PullToRefreshBase.Mode;
+import com.handmark.pulltorefresh.library.PullToRefreshBase.Orientation;
+import com.handmark.pulltorefresh.library.R;
+
+@SuppressLint("ViewConstructor")
+public class FlipLoadingLayout extends LoadingLayout {
+
+ static final int FLIP_ANIMATION_DURATION = 150;
+
+ private final Animation mRotateAnimation, mResetRotateAnimation;
+
+ public FlipLoadingLayout(Context context, final Mode mode, final Orientation scrollDirection, TypedArray attrs) {
+ super(context, mode, scrollDirection, attrs);
+
+ final int rotateAngle = mode == Mode.PULL_FROM_START ? -180 : 180;
+
+ mRotateAnimation = new RotateAnimation(0, rotateAngle, Animation.RELATIVE_TO_SELF, 0.5f,
+ Animation.RELATIVE_TO_SELF, 0.5f);
+ mRotateAnimation.setInterpolator(ANIMATION_INTERPOLATOR);
+ mRotateAnimation.setDuration(FLIP_ANIMATION_DURATION);
+ mRotateAnimation.setFillAfter(true);
+
+ mResetRotateAnimation = new RotateAnimation(rotateAngle, 0, Animation.RELATIVE_TO_SELF, 0.5f,
+ Animation.RELATIVE_TO_SELF, 0.5f);
+ mResetRotateAnimation.setInterpolator(ANIMATION_INTERPOLATOR);
+ mResetRotateAnimation.setDuration(FLIP_ANIMATION_DURATION);
+ mResetRotateAnimation.setFillAfter(true);
+ }
+
+ @Override
+ protected void onLoadingDrawableSet(Drawable imageDrawable) {
+ if (null != imageDrawable) {
+ final int dHeight = imageDrawable.getIntrinsicHeight();
+ final int dWidth = imageDrawable.getIntrinsicWidth();
+
+ /**
+ * We need to set the width/height of the ImageView so that it is
+ * square with each side the size of the largest drawable dimension.
+ * This is so that it doesn't clip when rotated.
+ */
+ ViewGroup.LayoutParams lp = mHeaderImage.getLayoutParams();
+ lp.width = lp.height = Math.max(dHeight, dWidth);
+ mHeaderImage.requestLayout();
+
+ /**
+ * We now rotate the Drawable so that is at the correct rotation,
+ * and is centered.
+ */
+ mHeaderImage.setScaleType(ScaleType.MATRIX);
+ Matrix matrix = new Matrix();
+ matrix.postTranslate((lp.width - dWidth) / 2f, (lp.height - dHeight) / 2f);
+ matrix.postRotate(getDrawableRotationAngle(), lp.width / 2f, lp.height / 2f);
+ mHeaderImage.setImageMatrix(matrix);
+ }
+ }
+
+ @Override
+ protected void onPullImpl(float scaleOfLayout) {
+ // NO-OP
+ }
+
+ @Override
+ protected void pullToRefreshImpl() {
+ // Only start reset Animation, we've previously show the rotate anim
+ if (mRotateAnimation == mHeaderImage.getAnimation()) {
+ mHeaderImage.startAnimation(mResetRotateAnimation);
+ }
+ }
+
+ @Override
+ protected void refreshingImpl() {
+ mHeaderImage.clearAnimation();
+ mHeaderImage.setVisibility(View.INVISIBLE);
+ mHeaderProgress.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ protected void releaseToRefreshImpl() {
+ mHeaderImage.startAnimation(mRotateAnimation);
+ }
+
+ @Override
+ protected void resetImpl() {
+ mHeaderImage.clearAnimation();
+ mHeaderProgress.setVisibility(View.GONE);
+ mHeaderImage.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ protected int getDefaultDrawableResId() {
+ return R.drawable.default_ptr_flip;
+ }
+
+ private float getDrawableRotationAngle() {
+ float angle = 0f;
+ switch (mMode) {
+ case PULL_FROM_END:
+ if (mScrollDirection == Orientation.HORIZONTAL) {
+ angle = 90f;
+ } else {
+ angle = 180f;
+ }
+ break;
+
+ case PULL_FROM_START:
+ if (mScrollDirection == Orientation.HORIZONTAL) {
+ angle = 270f;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return angle;
+ }
+
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/IndicatorLayout.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/IndicatorLayout.java
new file mode 100644
index 00000000..a9069f2a
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/IndicatorLayout.java
@@ -0,0 +1,147 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library.internal;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.RotateAnimation;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+
+import com.handmark.pulltorefresh.library.PullToRefreshBase;
+import com.handmark.pulltorefresh.library.R;
+
+@SuppressLint("ViewConstructor")
+public class IndicatorLayout extends FrameLayout implements AnimationListener {
+
+ static final int DEFAULT_ROTATION_ANIMATION_DURATION = 150;
+
+ private Animation mInAnim, mOutAnim;
+ private ImageView mArrowImageView;
+
+ private final Animation mRotateAnimation, mResetRotateAnimation;
+
+ public IndicatorLayout(Context context, PullToRefreshBase.Mode mode) {
+ super(context);
+ mArrowImageView = new ImageView(context);
+
+ Drawable arrowD = getResources().getDrawable(R.drawable.indicator_arrow);
+ mArrowImageView.setImageDrawable(arrowD);
+
+ final int padding = getResources().getDimensionPixelSize(R.dimen.indicator_internal_padding);
+ mArrowImageView.setPadding(padding, padding, padding, padding);
+ addView(mArrowImageView);
+
+ int inAnimResId, outAnimResId;
+ switch (mode) {
+ case PULL_FROM_END:
+ inAnimResId = R.anim.slide_in_from_bottom;
+ outAnimResId = R.anim.slide_out_to_bottom;
+ setBackgroundResource(R.drawable.indicator_bg_bottom);
+
+ // Rotate Arrow so it's pointing the correct way
+ mArrowImageView.setScaleType(ScaleType.MATRIX);
+ Matrix matrix = new Matrix();
+ matrix.setRotate(180f, arrowD.getIntrinsicWidth() / 2f, arrowD.getIntrinsicHeight() / 2f);
+ mArrowImageView.setImageMatrix(matrix);
+ break;
+ default:
+ case PULL_FROM_START:
+ inAnimResId = R.anim.slide_in_from_top;
+ outAnimResId = R.anim.slide_out_to_top;
+ setBackgroundResource(R.drawable.indicator_bg_top);
+ break;
+ }
+
+ mInAnim = AnimationUtils.loadAnimation(context, inAnimResId);
+ mInAnim.setAnimationListener(this);
+
+ mOutAnim = AnimationUtils.loadAnimation(context, outAnimResId);
+ mOutAnim.setAnimationListener(this);
+
+ final Interpolator interpolator = new LinearInterpolator();
+ mRotateAnimation = new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
+ 0.5f);
+ mRotateAnimation.setInterpolator(interpolator);
+ mRotateAnimation.setDuration(DEFAULT_ROTATION_ANIMATION_DURATION);
+ mRotateAnimation.setFillAfter(true);
+
+ mResetRotateAnimation = new RotateAnimation(-180, 0, Animation.RELATIVE_TO_SELF, 0.5f,
+ Animation.RELATIVE_TO_SELF, 0.5f);
+ mResetRotateAnimation.setInterpolator(interpolator);
+ mResetRotateAnimation.setDuration(DEFAULT_ROTATION_ANIMATION_DURATION);
+ mResetRotateAnimation.setFillAfter(true);
+
+ }
+
+ public final boolean isVisible() {
+ Animation currentAnim = getAnimation();
+ if (null != currentAnim) {
+ return mInAnim == currentAnim;
+ }
+
+ return getVisibility() == View.VISIBLE;
+ }
+
+ public void hide() {
+ startAnimation(mOutAnim);
+ }
+
+ public void show() {
+ mArrowImageView.clearAnimation();
+ startAnimation(mInAnim);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ if (animation == mOutAnim) {
+ mArrowImageView.clearAnimation();
+ setVisibility(View.GONE);
+ } else if (animation == mInAnim) {
+ setVisibility(View.VISIBLE);
+ }
+
+ clearAnimation();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ // NO-OP
+ }
+
+ @Override
+ public void onAnimationStart(Animation animation) {
+ setVisibility(View.VISIBLE);
+ }
+
+ public void releaseToRefresh() {
+ mArrowImageView.startAnimation(mRotateAnimation);
+ }
+
+ public void pullToRefresh() {
+ mArrowImageView.startAnimation(mResetRotateAnimation);
+ }
+
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/LoadingLayout.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/LoadingLayout.java
new file mode 100644
index 00000000..9c12586d
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/LoadingLayout.java
@@ -0,0 +1,393 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library.internal;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.handmark.pulltorefresh.library.ILoadingLayout;
+import com.handmark.pulltorefresh.library.PullToRefreshBase.Mode;
+import com.handmark.pulltorefresh.library.PullToRefreshBase.Orientation;
+import com.handmark.pulltorefresh.library.R;
+
+@SuppressLint("ViewConstructor")
+public abstract class LoadingLayout extends FrameLayout implements ILoadingLayout {
+
+ static final String LOG_TAG = "PullToRefresh-LoadingLayout";
+
+ static final Interpolator ANIMATION_INTERPOLATOR = new LinearInterpolator();
+
+ private FrameLayout mInnerLayout;
+
+ protected final ImageView mHeaderImage;
+ protected final ProgressBar mHeaderProgress;
+
+ private boolean mUseIntrinsicAnimation;
+
+ private final TextView mHeaderText;
+ private final TextView mSubHeaderText;
+
+ protected final Mode mMode;
+ protected final Orientation mScrollDirection;
+
+ private CharSequence mPullLabel;
+ private CharSequence mRefreshingLabel;
+ private CharSequence mReleaseLabel;
+
+ public LoadingLayout(Context context, final Mode mode, final Orientation scrollDirection, TypedArray attrs) {
+ super(context);
+ mMode = mode;
+ mScrollDirection = scrollDirection;
+
+ switch (scrollDirection) {
+ case HORIZONTAL:
+ LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header_horizontal, this);
+ break;
+ case VERTICAL:
+ default:
+ LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header_vertical, this);
+ break;
+ }
+
+ mInnerLayout = (FrameLayout) findViewById(R.id.fl_inner);
+ mHeaderText = (TextView) mInnerLayout.findViewById(R.id.pull_to_refresh_text);
+ mHeaderProgress = (ProgressBar) mInnerLayout.findViewById(R.id.pull_to_refresh_progress);
+ mSubHeaderText = (TextView) mInnerLayout.findViewById(R.id.pull_to_refresh_sub_text);
+ mHeaderImage = (ImageView) mInnerLayout.findViewById(R.id.pull_to_refresh_image);
+
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mInnerLayout.getLayoutParams();
+
+ switch (mode) {
+ case PULL_FROM_END:
+ lp.gravity = scrollDirection == Orientation.VERTICAL ? Gravity.TOP : Gravity.LEFT;
+
+ // Load in labels
+ mPullLabel = context.getString(R.string.pull_to_refresh_from_bottom_pull_label);
+ mRefreshingLabel = context.getString(R.string.pull_to_refresh_from_bottom_refreshing_label);
+ mReleaseLabel = context.getString(R.string.pull_to_refresh_from_bottom_release_label);
+ break;
+
+ case PULL_FROM_START:
+ default:
+ lp.gravity = scrollDirection == Orientation.VERTICAL ? Gravity.BOTTOM : Gravity.RIGHT;
+
+ // Load in labels
+ mPullLabel = context.getString(R.string.pull_to_refresh_pull_label);
+ mRefreshingLabel = context.getString(R.string.pull_to_refresh_refreshing_label);
+ mReleaseLabel = context.getString(R.string.pull_to_refresh_release_label);
+ break;
+ }
+
+ if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderBackground)) {
+ Drawable background = attrs.getDrawable(R.styleable.PullToRefresh_ptrHeaderBackground);
+ if (null != background) {
+ ViewCompat.setBackground(this, background);
+ }
+ }
+
+ if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderTextAppearance)) {
+ TypedValue styleID = new TypedValue();
+ attrs.getValue(R.styleable.PullToRefresh_ptrHeaderTextAppearance, styleID);
+ setTextAppearance(styleID.data);
+ }
+ if (attrs.hasValue(R.styleable.PullToRefresh_ptrSubHeaderTextAppearance)) {
+ TypedValue styleID = new TypedValue();
+ attrs.getValue(R.styleable.PullToRefresh_ptrSubHeaderTextAppearance, styleID);
+ setSubTextAppearance(styleID.data);
+ }
+
+ // Text Color attrs need to be set after TextAppearance attrs
+ if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderTextColor)) {
+ ColorStateList colors = attrs.getColorStateList(R.styleable.PullToRefresh_ptrHeaderTextColor);
+ if (null != colors) {
+ setTextColor(colors);
+ }
+ }
+ if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderSubTextColor)) {
+ ColorStateList colors = attrs.getColorStateList(R.styleable.PullToRefresh_ptrHeaderSubTextColor);
+ if (null != colors) {
+ setSubTextColor(colors);
+ }
+ }
+
+ // Try and get defined drawable from Attrs
+ Drawable imageDrawable = null;
+ if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawable)) {
+ imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawable);
+ }
+
+ // Check Specific Drawable from Attrs, these overrite the generic
+ // drawable attr above
+ switch (mode) {
+ case PULL_FROM_START:
+ default:
+ if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawableStart)) {
+ imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawableStart);
+ } else if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawableTop)) {
+ Utils.warnDeprecation("ptrDrawableTop", "ptrDrawableStart");
+ imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawableTop);
+ }
+ break;
+
+ case PULL_FROM_END:
+ if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawableEnd)) {
+ imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawableEnd);
+ } else if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawableBottom)) {
+ Utils.warnDeprecation("ptrDrawableBottom", "ptrDrawableEnd");
+ imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawableBottom);
+ }
+ break;
+ }
+
+ // If we don't have a user defined drawable, load the default
+ if (null == imageDrawable) {
+ imageDrawable = context.getResources().getDrawable(getDefaultDrawableResId());
+ }
+
+ // Set Drawable, and save width/height
+ setLoadingDrawable(imageDrawable);
+
+ reset();
+ }
+
+ public final void setHeight(int height) {
+ ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) getLayoutParams();
+ lp.height = height;
+ requestLayout();
+ }
+
+ public final void setWidth(int width) {
+ ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) getLayoutParams();
+ lp.width = width;
+ requestLayout();
+ }
+
+ public final int getContentSize() {
+ switch (mScrollDirection) {
+ case HORIZONTAL:
+ return mInnerLayout.getWidth();
+ case VERTICAL:
+ default:
+ return mInnerLayout.getHeight();
+ }
+ }
+
+ public final void hideAllViews() {
+ if (View.VISIBLE == mHeaderText.getVisibility()) {
+ mHeaderText.setVisibility(View.INVISIBLE);
+ }
+ if (View.VISIBLE == mHeaderProgress.getVisibility()) {
+ mHeaderProgress.setVisibility(View.INVISIBLE);
+ }
+ if (View.VISIBLE == mHeaderImage.getVisibility()) {
+ mHeaderImage.setVisibility(View.INVISIBLE);
+ }
+ if (View.VISIBLE == mSubHeaderText.getVisibility()) {
+ mSubHeaderText.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ public final void onPull(float scaleOfLayout) {
+ if (!mUseIntrinsicAnimation) {
+ onPullImpl(scaleOfLayout);
+ }
+ }
+
+ public final void pullToRefresh() {
+ if (null != mHeaderText) {
+ mHeaderText.setText(mPullLabel);
+ }
+
+ // Now call the callback
+ pullToRefreshImpl();
+ }
+
+ public final void refreshing() {
+ if (null != mHeaderText) {
+ mHeaderText.setText(mRefreshingLabel);
+ }
+
+ if (mUseIntrinsicAnimation) {
+ ((AnimationDrawable) mHeaderImage.getDrawable()).start();
+ } else {
+ // Now call the callback
+ refreshingImpl();
+ }
+
+ if (null != mSubHeaderText) {
+ mSubHeaderText.setVisibility(View.GONE);
+ }
+ }
+
+ public final void releaseToRefresh() {
+ if (null != mHeaderText) {
+ mHeaderText.setText(mReleaseLabel);
+ }
+
+ // Now call the callback
+ releaseToRefreshImpl();
+ }
+
+ public final void reset() {
+ if (null != mHeaderText) {
+ mHeaderText.setText(mPullLabel);
+ }
+ mHeaderImage.setVisibility(View.VISIBLE);
+
+ if (mUseIntrinsicAnimation) {
+ ((AnimationDrawable) mHeaderImage.getDrawable()).stop();
+ } else {
+ // Now call the callback
+ resetImpl();
+ }
+
+ if (null != mSubHeaderText) {
+ if (TextUtils.isEmpty(mSubHeaderText.getText())) {
+ mSubHeaderText.setVisibility(View.GONE);
+ } else {
+ mSubHeaderText.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ @Override
+ public void setLastUpdatedLabel(CharSequence label) {
+ setSubHeaderText(label);
+ }
+
+ public final void setLoadingDrawable(Drawable imageDrawable) {
+ // Set Drawable
+ mHeaderImage.setImageDrawable(imageDrawable);
+ mUseIntrinsicAnimation = (imageDrawable instanceof AnimationDrawable);
+
+ // Now call the callback
+ onLoadingDrawableSet(imageDrawable);
+ }
+
+ public void setPullLabel(CharSequence pullLabel) {
+ mPullLabel = pullLabel;
+ }
+
+ public void setRefreshingLabel(CharSequence refreshingLabel) {
+ mRefreshingLabel = refreshingLabel;
+ }
+
+ public void setReleaseLabel(CharSequence releaseLabel) {
+ mReleaseLabel = releaseLabel;
+ }
+
+ @Override
+ public void setTextTypeface(Typeface tf) {
+ mHeaderText.setTypeface(tf);
+ }
+
+ public final void showInvisibleViews() {
+ if (View.INVISIBLE == mHeaderText.getVisibility()) {
+ mHeaderText.setVisibility(View.VISIBLE);
+ }
+ if (View.INVISIBLE == mHeaderProgress.getVisibility()) {
+ mHeaderProgress.setVisibility(View.VISIBLE);
+ }
+ if (View.INVISIBLE == mHeaderImage.getVisibility()) {
+ mHeaderImage.setVisibility(View.VISIBLE);
+ }
+ if (View.INVISIBLE == mSubHeaderText.getVisibility()) {
+ mSubHeaderText.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Callbacks for derivative Layouts
+ */
+
+ protected abstract int getDefaultDrawableResId();
+
+ protected abstract void onLoadingDrawableSet(Drawable imageDrawable);
+
+ protected abstract void onPullImpl(float scaleOfLayout);
+
+ protected abstract void pullToRefreshImpl();
+
+ protected abstract void refreshingImpl();
+
+ protected abstract void releaseToRefreshImpl();
+
+ protected abstract void resetImpl();
+
+ private void setSubHeaderText(CharSequence label) {
+ if (null != mSubHeaderText) {
+ if (TextUtils.isEmpty(label)) {
+ mSubHeaderText.setVisibility(View.GONE);
+ } else {
+ mSubHeaderText.setText(label);
+
+ // Only set it to Visible if we're GONE, otherwise VISIBLE will
+ // be set soon
+ if (View.GONE == mSubHeaderText.getVisibility()) {
+ mSubHeaderText.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+ }
+
+ private void setSubTextAppearance(int value) {
+ if (null != mSubHeaderText) {
+ mSubHeaderText.setTextAppearance(getContext(), value);
+ }
+ }
+
+ private void setSubTextColor(ColorStateList color) {
+ if (null != mSubHeaderText) {
+ mSubHeaderText.setTextColor(color);
+ }
+ }
+
+ private void setTextAppearance(int value) {
+ if (null != mHeaderText) {
+ mHeaderText.setTextAppearance(getContext(), value);
+ }
+ if (null != mSubHeaderText) {
+ mSubHeaderText.setTextAppearance(getContext(), value);
+ }
+ }
+
+ private void setTextColor(ColorStateList color) {
+ if (null != mHeaderText) {
+ mHeaderText.setTextColor(color);
+ }
+ if (null != mSubHeaderText) {
+ mSubHeaderText.setTextColor(color);
+ }
+ }
+
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/RotateLoadingLayout.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/RotateLoadingLayout.java
new file mode 100644
index 00000000..bda7b2fc
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/RotateLoadingLayout.java
@@ -0,0 +1,110 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library.internal;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.view.animation.Animation;
+import android.view.animation.RotateAnimation;
+import android.widget.ImageView.ScaleType;
+
+import com.handmark.pulltorefresh.library.PullToRefreshBase.Mode;
+import com.handmark.pulltorefresh.library.PullToRefreshBase.Orientation;
+import com.handmark.pulltorefresh.library.R;
+
+public class RotateLoadingLayout extends LoadingLayout {
+
+ static final int ROTATION_ANIMATION_DURATION = 1200;
+
+ private final Animation mRotateAnimation;
+ private final Matrix mHeaderImageMatrix;
+
+ private float mRotationPivotX, mRotationPivotY;
+
+ private final boolean mRotateDrawableWhilePulling;
+
+ public RotateLoadingLayout(Context context, Mode mode, Orientation scrollDirection, TypedArray attrs) {
+ super(context, mode, scrollDirection, attrs);
+
+ mRotateDrawableWhilePulling = attrs.getBoolean(R.styleable.PullToRefresh_ptrRotateDrawableWhilePulling, true);
+
+ mHeaderImage.setScaleType(ScaleType.MATRIX);
+ mHeaderImageMatrix = new Matrix();
+ mHeaderImage.setImageMatrix(mHeaderImageMatrix);
+
+ mRotateAnimation = new RotateAnimation(0, 720, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
+ 0.5f);
+ mRotateAnimation.setInterpolator(ANIMATION_INTERPOLATOR);
+ mRotateAnimation.setDuration(ROTATION_ANIMATION_DURATION);
+ mRotateAnimation.setRepeatCount(Animation.INFINITE);
+ mRotateAnimation.setRepeatMode(Animation.RESTART);
+ }
+
+ public void onLoadingDrawableSet(Drawable imageDrawable) {
+ if (null != imageDrawable) {
+ mRotationPivotX = Math.round(imageDrawable.getIntrinsicWidth() / 2f);
+ mRotationPivotY = Math.round(imageDrawable.getIntrinsicHeight() / 2f);
+ }
+ }
+
+ protected void onPullImpl(float scaleOfLayout) {
+ float angle;
+ if (mRotateDrawableWhilePulling) {
+ angle = scaleOfLayout * 90f;
+ } else {
+ angle = Math.max(0f, Math.min(180f, scaleOfLayout * 360f - 180f));
+ }
+
+ mHeaderImageMatrix.setRotate(angle, mRotationPivotX, mRotationPivotY);
+ mHeaderImage.setImageMatrix(mHeaderImageMatrix);
+ }
+
+ @Override
+ protected void refreshingImpl() {
+ mHeaderImage.startAnimation(mRotateAnimation);
+ }
+
+ @Override
+ protected void resetImpl() {
+ mHeaderImage.clearAnimation();
+ resetImageRotation();
+ }
+
+ private void resetImageRotation() {
+ if (null != mHeaderImageMatrix) {
+ mHeaderImageMatrix.reset();
+ mHeaderImage.setImageMatrix(mHeaderImageMatrix);
+ }
+ }
+
+ @Override
+ protected void pullToRefreshImpl() {
+ // NO-OP
+ }
+
+ @Override
+ protected void releaseToRefreshImpl() {
+ // NO-OP
+ }
+
+ @Override
+ protected int getDefaultDrawableResId() {
+ return R.drawable.default_ptr_rotate;
+ }
+
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/Utils.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/Utils.java
new file mode 100644
index 00000000..73432189
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/Utils.java
@@ -0,0 +1,13 @@
+package com.handmark.pulltorefresh.library.internal;
+
+import android.util.Log;
+
+public class Utils {
+
+ static final String LOG_TAG = "PullToRefresh";
+
+ public static void warnDeprecation(String depreacted, String replacement) {
+ Log.w(LOG_TAG, "You're using the deprecated " + depreacted + " attr, please switch over to " + replacement);
+ }
+
+}
diff --git a/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/ViewCompat.java b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/ViewCompat.java
new file mode 100644
index 00000000..618bace0
--- /dev/null
+++ b/pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/ViewCompat.java
@@ -0,0 +1,70 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package com.handmark.pulltorefresh.library.internal;
+
+import android.annotation.TargetApi;
+import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.view.View;
+
+@SuppressWarnings("deprecation")
+public class ViewCompat {
+
+ public static void postOnAnimation(View view, Runnable runnable) {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
+ SDK16.postOnAnimation(view, runnable);
+ } else {
+ view.postDelayed(runnable, 16);
+ }
+ }
+
+ public static void setBackground(View view, Drawable background) {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
+ SDK16.setBackground(view, background);
+ } else {
+ view.setBackgroundDrawable(background);
+ }
+ }
+
+ public static void setLayerType(View view, int layerType) {
+ if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
+ SDK11.setLayerType(view, layerType);
+ }
+ }
+
+ @TargetApi(11)
+ static class SDK11 {
+
+ public static void setLayerType(View view, int layerType) {
+ view.setLayerType(layerType, null);
+ }
+ }
+
+ @TargetApi(16)
+ static class SDK16 {
+
+ public static void postOnAnimation(View view, Runnable runnable) {
+ view.postOnAnimation(runnable);
+ }
+
+ public static void setBackground(View view, Drawable background) {
+ view.setBackground(background);
+ }
+
+ }
+
+}
diff --git a/pulltorefresh/src/main/res/anim/slide_in_from_bottom.xml b/pulltorefresh/src/main/res/anim/slide_in_from_bottom.xml
new file mode 100644
index 00000000..bb430ce9
--- /dev/null
+++ b/pulltorefresh/src/main/res/anim/slide_in_from_bottom.xml
@@ -0,0 +1,21 @@
+
+
+
+
diff --git a/pulltorefresh/src/main/res/anim/slide_in_from_top.xml b/pulltorefresh/src/main/res/anim/slide_in_from_top.xml
new file mode 100644
index 00000000..52d91afc
--- /dev/null
+++ b/pulltorefresh/src/main/res/anim/slide_in_from_top.xml
@@ -0,0 +1,21 @@
+
+
+
+
diff --git a/pulltorefresh/src/main/res/anim/slide_out_to_bottom.xml b/pulltorefresh/src/main/res/anim/slide_out_to_bottom.xml
new file mode 100644
index 00000000..83eca5ad
--- /dev/null
+++ b/pulltorefresh/src/main/res/anim/slide_out_to_bottom.xml
@@ -0,0 +1,21 @@
+
+
+
+
diff --git a/pulltorefresh/src/main/res/anim/slide_out_to_top.xml b/pulltorefresh/src/main/res/anim/slide_out_to_top.xml
new file mode 100644
index 00000000..5105ae1f
--- /dev/null
+++ b/pulltorefresh/src/main/res/anim/slide_out_to_top.xml
@@ -0,0 +1,21 @@
+
+
+
+
diff --git a/pulltorefresh/src/main/res/drawable-hdpi/default_ptr_flip.png b/pulltorefresh/src/main/res/drawable-hdpi/default_ptr_flip.png
new file mode 100644
index 00000000..0a2c0bd9
Binary files /dev/null and b/pulltorefresh/src/main/res/drawable-hdpi/default_ptr_flip.png differ
diff --git a/pulltorefresh/src/main/res/drawable-hdpi/default_ptr_rotate.png b/pulltorefresh/src/main/res/drawable-hdpi/default_ptr_rotate.png
new file mode 100644
index 00000000..dc641b72
Binary files /dev/null and b/pulltorefresh/src/main/res/drawable-hdpi/default_ptr_rotate.png differ
diff --git a/pulltorefresh/src/main/res/drawable-hdpi/indicator_arrow.png b/pulltorefresh/src/main/res/drawable-hdpi/indicator_arrow.png
new file mode 100644
index 00000000..8ae79770
Binary files /dev/null and b/pulltorefresh/src/main/res/drawable-hdpi/indicator_arrow.png differ
diff --git a/pulltorefresh/src/main/res/drawable-mdpi/default_ptr_flip.png b/pulltorefresh/src/main/res/drawable-mdpi/default_ptr_flip.png
new file mode 100644
index 00000000..be696c1c
Binary files /dev/null and b/pulltorefresh/src/main/res/drawable-mdpi/default_ptr_flip.png differ
diff --git a/pulltorefresh/src/main/res/drawable-mdpi/default_ptr_rotate.png b/pulltorefresh/src/main/res/drawable-mdpi/default_ptr_rotate.png
new file mode 100644
index 00000000..95b22bd7
Binary files /dev/null and b/pulltorefresh/src/main/res/drawable-mdpi/default_ptr_rotate.png differ
diff --git a/pulltorefresh/src/main/res/drawable-mdpi/indicator_arrow.png b/pulltorefresh/src/main/res/drawable-mdpi/indicator_arrow.png
new file mode 100644
index 00000000..20fe2c12
Binary files /dev/null and b/pulltorefresh/src/main/res/drawable-mdpi/indicator_arrow.png differ
diff --git a/pulltorefresh/src/main/res/drawable-xhdpi/default_ptr_flip.png b/pulltorefresh/src/main/res/drawable-xhdpi/default_ptr_flip.png
new file mode 100644
index 00000000..3e6ddba5
Binary files /dev/null and b/pulltorefresh/src/main/res/drawable-xhdpi/default_ptr_flip.png differ
diff --git a/pulltorefresh/src/main/res/drawable-xhdpi/default_ptr_rotate.png b/pulltorefresh/src/main/res/drawable-xhdpi/default_ptr_rotate.png
new file mode 100644
index 00000000..00225c9a
Binary files /dev/null and b/pulltorefresh/src/main/res/drawable-xhdpi/default_ptr_rotate.png differ
diff --git a/pulltorefresh/src/main/res/drawable-xhdpi/indicator_arrow.png b/pulltorefresh/src/main/res/drawable-xhdpi/indicator_arrow.png
new file mode 100644
index 00000000..810ff595
Binary files /dev/null and b/pulltorefresh/src/main/res/drawable-xhdpi/indicator_arrow.png differ
diff --git a/pulltorefresh/src/main/res/drawable/indicator_bg_bottom.xml b/pulltorefresh/src/main/res/drawable/indicator_bg_bottom.xml
new file mode 100644
index 00000000..81a91aa6
--- /dev/null
+++ b/pulltorefresh/src/main/res/drawable/indicator_bg_bottom.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pulltorefresh/src/main/res/drawable/indicator_bg_top.xml b/pulltorefresh/src/main/res/drawable/indicator_bg_top.xml
new file mode 100644
index 00000000..59fa9cfe
--- /dev/null
+++ b/pulltorefresh/src/main/res/drawable/indicator_bg_top.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pulltorefresh/src/main/res/layout/pull_to_refresh_header_horizontal.xml b/pulltorefresh/src/main/res/layout/pull_to_refresh_header_horizontal.xml
new file mode 100644
index 00000000..f05bb033
--- /dev/null
+++ b/pulltorefresh/src/main/res/layout/pull_to_refresh_header_horizontal.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pulltorefresh/src/main/res/layout/pull_to_refresh_header_vertical.xml b/pulltorefresh/src/main/res/layout/pull_to_refresh_header_vertical.xml
new file mode 100644
index 00000000..2185e2d6
--- /dev/null
+++ b/pulltorefresh/src/main/res/layout/pull_to_refresh_header_vertical.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pulltorefresh/src/main/res/values-ar/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-ar/pull_refresh_strings.xml
new file mode 100644
index 00000000..5b0b571c
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-ar/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ اسحب للتحديث…
+ اترك للتحديث…
+ تحميل…
+
diff --git a/pulltorefresh/src/main/res/values-cs/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-cs/pull_refresh_strings.xml
new file mode 100755
index 00000000..a8f0acea
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-cs/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ Tažením aktualizujete…
+ Uvolněním aktualizujete…
+ Načítání…
+
diff --git a/pulltorefresh/src/main/res/values-de/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-de/pull_refresh_strings.xml
new file mode 100755
index 00000000..3f773deb
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-de/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ Ziehen zum Aktualisieren…
+ Loslassen zum Aktualisieren…
+ Laden…
+
diff --git a/pulltorefresh/src/main/res/values-es/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-es/pull_refresh_strings.xml
new file mode 100755
index 00000000..6f85c100
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-es/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ Tirar para actualizar…
+ Soltar para actualizar…
+ Cargando…
+
diff --git a/pulltorefresh/src/main/res/values-fi/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-fi/pull_refresh_strings.xml
new file mode 100755
index 00000000..0381287b
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-fi/pull_refresh_strings.xml
@@ -0,0 +1,13 @@
+
+
+
+ Päivitä vetämällä alas…
+ Päivitä vapauttamalla…
+ Päivitetään…
+
+
+ Päivitä vetämällä ylös…
+ @string/pull_to_refresh_release_label
+ @string/pull_to_refresh_refreshing_label
+
+
\ No newline at end of file
diff --git a/pulltorefresh/src/main/res/values-fr/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-fr/pull_refresh_strings.xml
new file mode 100755
index 00000000..e59f07ec
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-fr/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ Tirez pour rafraîchir…
+ Relâcher pour rafraîchir…
+ Chargement…
+
diff --git a/pulltorefresh/src/main/res/values-he/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-he/pull_refresh_strings.xml
new file mode 100644
index 00000000..f22f7853
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-he/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ משוך לרענון…
+ שחרר לרענון…
+ טוען…
+
diff --git a/pulltorefresh/src/main/res/values-it/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-it/pull_refresh_strings.xml
new file mode 100755
index 00000000..9eaa7495
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-it/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ Tira per aggiornare…
+ Rilascia per aggionare…
+ Caricamento…
+
diff --git a/pulltorefresh/src/main/res/values-iw/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-iw/pull_refresh_strings.xml
new file mode 100644
index 00000000..f22f7853
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-iw/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ משוך לרענון…
+ שחרר לרענון…
+ טוען…
+
diff --git a/pulltorefresh/src/main/res/values-ja/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-ja/pull_refresh_strings.xml
new file mode 100644
index 00000000..12415443
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-ja/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ 画面を引っ張って…
+ 指を離して更新…
+ 読み込み中…
+
diff --git a/pulltorefresh/src/main/res/values-ko/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-ko/pull_refresh_strings.xml
new file mode 100755
index 00000000..15d86d4d
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-ko/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ 당겨서 새로 고침…
+ 놓아서 새로 고침…
+ 로드 중…
+
diff --git a/pulltorefresh/src/main/res/values-nl/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-nl/pull_refresh_strings.xml
new file mode 100755
index 00000000..0701c9b4
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-nl/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ Sleep om te vernieuwen…
+ Loslaten om te vernieuwen…
+ Laden…
+
diff --git a/pulltorefresh/src/main/res/values-pl/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-pl/pull_refresh_strings.xml
new file mode 100755
index 00000000..7d474c18
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-pl/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ Pociągnij, aby odświeżyć…
+ Puść, aby odświeżyć…
+ Wczytywanie…
+
diff --git a/pulltorefresh/src/main/res/values-pt-rBR/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-pt-rBR/pull_refresh_strings.xml
new file mode 100755
index 00000000..1a4359a8
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-pt-rBR/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ Puxe para atualizar…
+ Libere para atualizar…
+ Carregando…
+
diff --git a/pulltorefresh/src/main/res/values-pt/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-pt/pull_refresh_strings.xml
new file mode 100755
index 00000000..71eea31a
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-pt/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ Puxe para atualizar…
+ Liberação para atualizar…
+ A carregar…
+
diff --git a/pulltorefresh/src/main/res/values-ro/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-ro/pull_refresh_strings.xml
new file mode 100644
index 00000000..c89e80a9
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-ro/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ Trage pentru a reîmprospăta…
+ Eliberează pentru a reîmprospăta…
+ Încărcare…
+
diff --git a/pulltorefresh/src/main/res/values-ru/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-ru/pull_refresh_strings.xml
new file mode 100755
index 00000000..ca364ff8
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-ru/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ Потяните для обновления…
+ Отпустите для обновления…
+ Загрузка…
+
diff --git a/pulltorefresh/src/main/res/values-zh/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values-zh/pull_refresh_strings.xml
new file mode 100755
index 00000000..397376be
--- /dev/null
+++ b/pulltorefresh/src/main/res/values-zh/pull_refresh_strings.xml
@@ -0,0 +1,6 @@
+
+
+ 下拉刷新…
+ 放开以刷新…
+ 正在载入…
+
diff --git a/pulltorefresh/src/main/res/values/attrs.xml b/pulltorefresh/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..a3408ae7
--- /dev/null
+++ b/pulltorefresh/src/main/res/values/attrs.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pulltorefresh/src/main/res/values/dimens.xml b/pulltorefresh/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..24339b5b
--- /dev/null
+++ b/pulltorefresh/src/main/res/values/dimens.xml
@@ -0,0 +1,10 @@
+
+
+
+ 10dp
+ 12dp
+ 4dp
+ 24dp
+ 12dp
+
+
\ No newline at end of file
diff --git a/pulltorefresh/src/main/res/values/ids.xml b/pulltorefresh/src/main/res/values/ids.xml
new file mode 100644
index 00000000..26c9ae76
--- /dev/null
+++ b/pulltorefresh/src/main/res/values/ids.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pulltorefresh/src/main/res/values/pull_refresh_strings.xml b/pulltorefresh/src/main/res/values/pull_refresh_strings.xml
new file mode 100755
index 00000000..fe48f3bc
--- /dev/null
+++ b/pulltorefresh/src/main/res/values/pull_refresh_strings.xml
@@ -0,0 +1,13 @@
+
+
+
+ Pull to refresh…
+ Release to refresh…
+ Loading…
+
+
+ @string/pull_to_refresh_pull_label
+ @string/pull_to_refresh_release_label
+ @string/pull_to_refresh_refreshing_label
+
+
\ No newline at end of file