diff --git a/menudrawer/build.gradle b/menudrawer/build.gradle new file mode 100644 index 00000000..bc827b38 --- /dev/null +++ b/menudrawer/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/menudrawer/menudrawer.iml b/menudrawer/menudrawer.iml new file mode 100644 index 00000000..e83a8085 --- /dev/null +++ b/menudrawer/menudrawer.iml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/menudrawer/src/main/AndroidManifest.xml b/menudrawer/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0dc0710c --- /dev/null +++ b/menudrawer/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/menudrawer/src/main/java/net/simonvt/menudrawer/BuildLayerFrameLayout.java b/menudrawer/src/main/java/net/simonvt/menudrawer/BuildLayerFrameLayout.java new file mode 100644 index 00000000..14ee9153 --- /dev/null +++ b/menudrawer/src/main/java/net/simonvt/menudrawer/BuildLayerFrameLayout.java @@ -0,0 +1,99 @@ +package net.simonvt.menudrawer; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * FrameLayout which caches the hardware layer if available. + *

+ * 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