From a4c38a073c7c7f793d0b79ac3b8bf89e3aac048e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Garc=C3=ADa=20Amor?= Date: Sun, 26 Jul 2015 17:21:54 +0200 Subject: [PATCH] Missing submodule files added --- menudrawer/build.gradle | 18 + menudrawer/menudrawer.iml | 91 + menudrawer/src/main/AndroidManifest.xml | 8 + .../menudrawer/BuildLayerFrameLayout.java | 99 + .../net/simonvt/menudrawer/ColorDrawable.java | 170 ++ .../simonvt/menudrawer/DraggableDrawer.java | 619 ++++++ .../net/simonvt/menudrawer/FloatScroller.java | 175 ++ .../net/simonvt/menudrawer/MenuDrawer.java | 1652 ++++++++++++++++ .../menudrawer/NoClickThroughFrameLayout.java | 28 + .../net/simonvt/menudrawer/OverlayDrawer.java | 781 ++++++++ .../simonvt/menudrawer/PeekInterpolator.java | 28 + .../java/net/simonvt/menudrawer/Position.java | 50 + .../java/net/simonvt/menudrawer/Scroller.java | 505 +++++ .../menudrawer/SinusoidalInterpolator.java | 15 + .../net/simonvt/menudrawer/SlideDrawable.java | 187 ++ .../net/simonvt/menudrawer/SlidingDrawer.java | 707 +++++++ .../menudrawer/SmoothInterpolator.java | 12 + .../net/simonvt/menudrawer/StaticDrawer.java | 218 +++ .../net/simonvt/menudrawer/ViewHelper.java | 50 + .../menudrawer/compat/ActionBarHelper.java | 83 + .../compat/ActionBarHelperCompat.java | 107 ++ .../compat/ActionBarHelperNative.java | 114 ++ menudrawer/src/main/res/values/attrs.xml | 65 + menudrawer/src/main/res/values/colors.xml | 6 + menudrawer/src/main/res/values/ids.xml | 24 + menudrawer/src/main/res/values/strings.xml | 8 + menudrawer/src/main/res/values/styles.xml | 13 + pulltorefresh/build.gradle | 18 + pulltorefresh/pulltorefresh.iml | 100 + pulltorefresh/src/main/AndroidManifest.xml | 8 + .../pulltorefresh/library/ILoadingLayout.java | 57 + .../pulltorefresh/library/IPullToRefresh.java | 246 +++ .../library/LoadingLayoutProxy.java | 73 + .../library/OverscrollHelper.java | 178 ++ .../library/PullToRefreshAdapterViewBase.java | 475 +++++ .../library/PullToRefreshBase.java | 1653 +++++++++++++++++ .../PullToRefreshExpandableListView.java | 103 + .../library/PullToRefreshGridView.java | 102 + .../PullToRefreshHorizontalScrollView.java | 110 ++ .../library/PullToRefreshListView.java | 337 ++++ .../library/PullToRefreshScrollView.java | 109 ++ .../library/PullToRefreshWebView.java | 165 ++ .../library/extras/PullToRefreshWebView2.java | 132 ++ .../extras/SoundPullEventListener.java | 96 + .../internal/EmptyViewMethodAccessor.java | 43 + .../library/internal/FlipLoadingLayout.java | 146 ++ .../library/internal/IndicatorLayout.java | 147 ++ .../library/internal/LoadingLayout.java | 393 ++++ .../library/internal/RotateLoadingLayout.java | 110 ++ .../pulltorefresh/library/internal/Utils.java | 13 + .../library/internal/ViewCompat.java | 70 + .../main/res/anim/slide_in_from_bottom.xml | 21 + .../src/main/res/anim/slide_in_from_top.xml | 21 + .../src/main/res/anim/slide_out_to_bottom.xml | 21 + .../src/main/res/anim/slide_out_to_top.xml | 21 + .../res/drawable-hdpi/default_ptr_flip.png | Bin 0 -> 1835 bytes .../res/drawable-hdpi/default_ptr_rotate.png | Bin 0 -> 48963 bytes .../res/drawable-hdpi/indicator_arrow.png | Bin 0 -> 390 bytes .../res/drawable-mdpi/default_ptr_flip.png | Bin 0 -> 1612 bytes .../res/drawable-mdpi/default_ptr_rotate.png | Bin 0 -> 49665 bytes .../res/drawable-mdpi/indicator_arrow.png | Bin 0 -> 445 bytes .../res/drawable-xhdpi/default_ptr_flip.png | Bin 0 -> 1983 bytes .../res/drawable-xhdpi/default_ptr_rotate.png | Bin 0 -> 50019 bytes .../res/drawable-xhdpi/indicator_arrow.png | Bin 0 -> 429 bytes .../main/res/drawable/indicator_bg_bottom.xml | 18 + .../main/res/drawable/indicator_bg_top.xml | 18 + .../pull_to_refresh_header_horizontal.xml | 29 + .../pull_to_refresh_header_vertical.xml | 59 + .../res/values-ar/pull_refresh_strings.xml | 6 + .../res/values-cs/pull_refresh_strings.xml | 6 + .../res/values-de/pull_refresh_strings.xml | 6 + .../res/values-es/pull_refresh_strings.xml | 6 + .../res/values-fi/pull_refresh_strings.xml | 13 + .../res/values-fr/pull_refresh_strings.xml | 6 + .../res/values-he/pull_refresh_strings.xml | 6 + .../res/values-it/pull_refresh_strings.xml | 6 + .../res/values-iw/pull_refresh_strings.xml | 6 + .../res/values-ja/pull_refresh_strings.xml | 6 + .../res/values-ko/pull_refresh_strings.xml | 6 + .../res/values-nl/pull_refresh_strings.xml | 6 + .../res/values-pl/pull_refresh_strings.xml | 6 + .../values-pt-rBR/pull_refresh_strings.xml | 6 + .../res/values-pt/pull_refresh_strings.xml | 6 + .../res/values-ro/pull_refresh_strings.xml | 6 + .../res/values-ru/pull_refresh_strings.xml | 6 + .../res/values-zh/pull_refresh_strings.xml | 6 + pulltorefresh/src/main/res/values/attrs.xml | 80 + pulltorefresh/src/main/res/values/dimens.xml | 10 + pulltorefresh/src/main/res/values/ids.xml | 8 + .../main/res/values/pull_refresh_strings.xml | 13 + 90 files changed, 11151 insertions(+) create mode 100644 menudrawer/build.gradle create mode 100644 menudrawer/menudrawer.iml create mode 100644 menudrawer/src/main/AndroidManifest.xml create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/BuildLayerFrameLayout.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/ColorDrawable.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/DraggableDrawer.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/FloatScroller.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/MenuDrawer.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/NoClickThroughFrameLayout.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/OverlayDrawer.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/PeekInterpolator.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/Position.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/Scroller.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/SinusoidalInterpolator.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/SlideDrawable.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/SlidingDrawer.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/SmoothInterpolator.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/StaticDrawer.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/ViewHelper.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/compat/ActionBarHelperCompat.java create mode 100644 menudrawer/src/main/java/net/simonvt/menudrawer/compat/ActionBarHelperNative.java create mode 100644 menudrawer/src/main/res/values/attrs.xml create mode 100644 menudrawer/src/main/res/values/colors.xml create mode 100644 menudrawer/src/main/res/values/ids.xml create mode 100644 menudrawer/src/main/res/values/strings.xml create mode 100644 menudrawer/src/main/res/values/styles.xml create mode 100644 pulltorefresh/build.gradle create mode 100644 pulltorefresh/pulltorefresh.iml create mode 100644 pulltorefresh/src/main/AndroidManifest.xml create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/ILoadingLayout.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/IPullToRefresh.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/LoadingLayoutProxy.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/OverscrollHelper.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshAdapterViewBase.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshBase.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshExpandableListView.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshGridView.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshHorizontalScrollView.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshListView.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshScrollView.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/PullToRefreshWebView.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/extras/PullToRefreshWebView2.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/extras/SoundPullEventListener.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/EmptyViewMethodAccessor.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/FlipLoadingLayout.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/IndicatorLayout.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/LoadingLayout.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/RotateLoadingLayout.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/Utils.java create mode 100644 pulltorefresh/src/main/java/com/handmark/pulltorefresh/library/internal/ViewCompat.java create mode 100644 pulltorefresh/src/main/res/anim/slide_in_from_bottom.xml create mode 100644 pulltorefresh/src/main/res/anim/slide_in_from_top.xml create mode 100644 pulltorefresh/src/main/res/anim/slide_out_to_bottom.xml create mode 100644 pulltorefresh/src/main/res/anim/slide_out_to_top.xml create mode 100644 pulltorefresh/src/main/res/drawable-hdpi/default_ptr_flip.png create mode 100644 pulltorefresh/src/main/res/drawable-hdpi/default_ptr_rotate.png create mode 100644 pulltorefresh/src/main/res/drawable-hdpi/indicator_arrow.png create mode 100644 pulltorefresh/src/main/res/drawable-mdpi/default_ptr_flip.png create mode 100644 pulltorefresh/src/main/res/drawable-mdpi/default_ptr_rotate.png create mode 100644 pulltorefresh/src/main/res/drawable-mdpi/indicator_arrow.png create mode 100644 pulltorefresh/src/main/res/drawable-xhdpi/default_ptr_flip.png create mode 100644 pulltorefresh/src/main/res/drawable-xhdpi/default_ptr_rotate.png create mode 100644 pulltorefresh/src/main/res/drawable-xhdpi/indicator_arrow.png create mode 100644 pulltorefresh/src/main/res/drawable/indicator_bg_bottom.xml create mode 100644 pulltorefresh/src/main/res/drawable/indicator_bg_top.xml create mode 100644 pulltorefresh/src/main/res/layout/pull_to_refresh_header_horizontal.xml create mode 100644 pulltorefresh/src/main/res/layout/pull_to_refresh_header_vertical.xml create mode 100644 pulltorefresh/src/main/res/values-ar/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-cs/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-de/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-es/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-fi/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-fr/pull_refresh_strings.xml create mode 100644 pulltorefresh/src/main/res/values-he/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-it/pull_refresh_strings.xml create mode 100644 pulltorefresh/src/main/res/values-iw/pull_refresh_strings.xml create mode 100644 pulltorefresh/src/main/res/values-ja/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-ko/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-nl/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-pl/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-pt-rBR/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-pt/pull_refresh_strings.xml create mode 100644 pulltorefresh/src/main/res/values-ro/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-ru/pull_refresh_strings.xml create mode 100755 pulltorefresh/src/main/res/values-zh/pull_refresh_strings.xml create mode 100644 pulltorefresh/src/main/res/values/attrs.xml create mode 100644 pulltorefresh/src/main/res/values/dimens.xml create mode 100644 pulltorefresh/src/main/res/values/ids.xml create mode 100755 pulltorefresh/src/main/res/values/pull_refresh_strings.xml 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 0000000000000000000000000000000000000000..0a2c0bd9bb4907fc43e8ba68e3b9b6fefd6d84ea GIT binary patch literal 1835 zcmZXU2{fDO8pnesN{d@dgm5jfs~U8u1Ytxa$RdlSt;RB=RYhsIG)=2BsuJy3+o=|v zS{lo-v|4Iks$}d*89`$kEe1ntZ3Xwc*S*d?=RW7W|L^zw-~V&Yci#7$mrWwL%E_wB zfZ=j^Oaii!vhx%>kNbn4h`+C;<{RoQ_Z zK`wxzwS8RgaNbtXC}(To>SY}eTt?xN8?)V7X#_6&wryDK+KZ*PUvK;Exvth}>6Bv~r?3oBbOmfvY;PtZZj{5)W? zpi*|Y0b*y!O`YZ@!=mGp{QT)<86R?1hwb;(#Q&$Z%o7Gr$Yl};~H9*2yBnfxo< zBlzu0(H2Y%*mM56{=!2Z;?!{T1;wfY)%azvt(U@}xwIl4C1f&Svek`Ml0|Gd%| z)LnHZt49)%@_jS6cP3ss9{sYcp$Ob7K-yU4K|i~DhlZCIJB^k<^7TBjW0{3IpTaW^ ztH0ltsnsoC7xPrD8|v1k z<#`sOIxDU;|MuJmO&O68YXw#nvF1qY}Foq?oL zkd+hLn)B|{B1R2AHhz=VZ-_x@*{JW8+o+@Hx_eOm536WjP-N_7xTzBGY{*=}`d$Tr zjwt?XNeDzY#2}D_JjsLVvbVQ~!{P9FJdH;4{CD{hd;unt$pivnABaRE(2_``gUmiq zC=_4IA z<9C?T?jCpo(UVN2dHebY1&4%1T#1T}zm<^0zWeZTZe9Vmq`dM)bzNgi+uPrI`uRh` z_oB&}xrLS0&HlJPCUE{{IE)h&rdHNE@8u)`0}mkH^kz=wrRtW)VG9b#o|)8>^E>nc zjHcG`nHxel2zo4<&C z6qe=Vbihi=3CaXd7+KHx81@!R*57{Gh2t!6D{yktB+HV&CJ9mpwTpdn-r}IxFF{?- zCfOtj@~2Ers@nzXkKbQSlPkTdl?o?MXDg<@JR;E6H#e3sX1Ro5vqHNZyT(aSayj1C zwb>T7 a+Ju$p-kaba>tx`1KsXlyru;kqAO8vU6*3Y4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dc641b72459953994dd5800438f559dc4659f228 GIT binary patch literal 48963 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

    Ys7l zAukqY^p64u8u$6luCcnyh~(9;R@n~S8EcdVgy@fQN+h)n31;XM$4|D6Nt4XJ++$9S z);5*ajBeYsuYdqEK7Yfm2UeIF#w*l}5_2+UfkUp)`T$|-#<(>WZq_Cx-$1Be1oMBPSq&E}3U z_pk1zP3%6kaZ6J5L_N|vRCT`Pm5JVT;JpoXj%X^}fYEv4p?80uVOkt6EX6kFE1HL2 zy2t7P@4z%~bM2jx*2#(IFhxP_l)H@u7Qv_T^;%(`OV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8ae79770973e4fd85f994c0f6edff5eb5ebae6c9 GIT binary patch literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^LO`s}!VDxogtJxwDdu7)&kzm{j@u9Y9{{O>_%)r16w3}Oq z@zUM8KR`j564!{5;QX|b^2DN42FH~Aq*MjB%%art{G#k)1?OPX!mW?Zfa=zIx;TbN zOifNmNDu*n1O`SmRaI416EP+Z#-kz!brO@4lbsE`j~qRERPc<`jHZOnwzf95MH>aG z1RZpNfU|dL91yfD<4c_F=I-t;EXDoMuI065`nfrljJ-#7nVA94ek1j`Rw2C;>ZYolBP$$cBi;Bqs#Pm zN_H)lBW3aKDhEO(ckt`#k8Gj}8yV^wZ;TTVbrl(}FBuK1`; zbs$4tSCBGH4>PDm98In)xfOW2YuF|!qS{WTVf`R-e$wyqf#vkvnnTYt5(c*}(&`hi zZ=cK?&fv=#Wi`0K3yPAYCDA##XgGPk@D`gn_}PuYDA67S4=S>Rr}EYIEuVnGzKuMlRhgp`JMAT@5+52%E)YQ0x?;QQNX$-|+iG!b%JG^p&Q0lhs!2?_>Ov1Y1JuZa1aEJ2uot zJ<{udd9(`0H(w|@8gp!8pIEUjQ>X8*hXbNXCRq2Z7|TW5d;%Nx`}I(Qo@O_?jk&m( z5MWk8uM0geIiyVc)MnlvNCz`}LEM$UI|A(xM3L6XfwQ}kKD5PMf2_l^$|C1t+R9Ef zJg+!_rS`6q!h`NS2b0QpKAOKlQT22v8fy89ZLu)NB}URLKTAck}2QTt3(lS;ISk>kO2ugugZ!ryy$_fh&PvU9)u zbFtfGGaoD1{X$eT++DPqwOSZfz$nn^8N!^1)5GMMvMt$LS1j1(Y?~EN4%539v100X zyFS@kaaY|m@1dEPRmKQ}3~>C8zYcl0!<s_M; z)`x!eb-5~Q!c2G__bq_1r!*HD^!wsx2lo- zwJde0+QWLMD@e2!KMNtd#&lBofyYz(%D~ zf7X8()&P8gE`Za10x+!MR|Tm2C;!N6&R_jB8s%5R8vnm5@^R%mUt7i#RgJMV_Cm74*E! ziJwKx+J;$JL%-amv~4fVxftw^hkQuCb~%F3!k1J??k#3i*_9Xsllcu$IOMunBXc@8 zF`}!bC0b`BFeYK%cExS{(4`6m+F4Ci;@)9z3$F5&{l+Lkj=jK^g|W|0sfW8`Y^6JG z4~Km7wl_w}`W{q5tQ(H#A+q6m6?dhdv^-!JeAU{sUdQk_J2k_Rc5ZppQMhZqZHjDe Rga~W{!nqP$O7{j_`WN%b-~Ipq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..95b22bd71c4f2ed6c91652ac255b62bb8461e38a GIT binary patch literal 49665 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%0A-uWX&~N5)pN#W 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..20fe2c126ae700d57be9c7f19a0cf7296ece8055 GIT binary patch literal 445 zcmV;u0Yd(XP)=Zx2#&4L) zK8=5n)@r~%(8dq25D}Ygf)-ju1T8`$8bu5W7I)FRVfVa&E$+=eW_IVcMlM984Kygm zR8>c_kGe@VfEFcTg=Zi!?7K`@0hSD#0CzFr3z!C2w8AiJtFq7m+J=pQ%b4&1yaiaa z!aeY0Sm8`o*aDUfdjw8n!Yj}WuxN!bW>COOnXn108a4p>F`*AU7*?2z6%t@Rz@owq zux{8ba1|3mzjxt%SfL3l1lVuXMC1fmq*}b!fi7@VQP@ws_haYWC$LR=L$P(Js^B*8$cdn=e$g2QZf_cR)JTmRW-=mmWIbponY$$NatP39zTCH?uv3DSGdR&bcwL no3+wa)w6%ELPS(VG?e=R;L=SFhh?BF00000NkvXXu0mjf7aXr< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3e6ddba5c554ba6983077079125c30387e70fed8 GIT binary patch literal 1983 zcmZXUcT`hp8ixaz&_M|aU8FN0B1Mn{h%R9u1d@;j48~A~8j5H{5Tayg23#4Mqo{C1 zkR}j}h++^y+7OT;B1M`5Dhi6ULF(S!oRhO>_dVx(zUO}L@4bKAd(Y(%a4zyP>M|e@ zNFL*gCIY)Yun1BT0Dq1&Vu3*W^uwH-2pA`)Lxi)@!C?_WAkgsxsg$Ycak8=_Cz@CS zM;-EZNzZ7nrI37|YN%ItKrp$bi*E4J`4JQSS%fX8LPK(V{&6Kffo%)f7CSL2q3CL_ zBM%8k7+rty2t!IYsl>P@d}>qFFC?~8EraJ3dSndphOiPtRr5HrY(cNmkrvqBufEJ^Wk?AYv%l`**7ce%X1*KX z=;B{Q!L(Rv8*-jKLyX5*7b;q^_zTw zRZH<9_WY4x^9Fyqglbpr3?)jVQ8zf7SDoX3wr|EfE2hC>Pw~jD^Q#NAWbduprB4H& zC~_uE*U8U0sJHd2noC$Nz4j@_|1x+ZYjZGBJWktlwIYYboc!oQr&lXaLMFip182%) zJ+{KnTO??z)(xx~e7Vu3cl_=75G7uTO42v##-owIPub;Nq_AoK=@vIuRtY4eUdS{a)lTeSU`Q_l=L4>VUF?<>8#fYk@I5}2R;NvN~V|f#KI;-oa z$Y=SjdD;Sn#@M@R0=Qepz`Wt*>fnnZ2gXLg&k9w>elMCBTwp>}<3~_@g!u=E<~VJL z81VSs3ztu5v>py_7VxIR4Ov1cw9IR3`q{?bz_KeP^`*eyLKi=%6fqHRm-Sa?%GHM| zO=iWOrO3|OGtGb2d80_9V%ypQyoIA0A&)VR`!m%lL}ttI==jo4r}PX$4%Y>8c*S-@@8=5qd}x8hYM#(6NlcKrJZPcSD|S zyjpzDU@*61R{lAW8QE#Wjp}&Zx-n8)XSN+l6i519%(#BHdnvY6I!;f^;gr~G1Nkq} z(?0!U#Id@2rJsMW`}GW4P+g8agHLDrD|d@`E27HjDk#OA!vnq3W+x+c^_8MH} zr}TrlWwT973)1Voaywd0s!SPPxJP}0+4p^_)`|v@JCL^>(Fou`5-4FnNb-LQXaNN%&=sDDC~S!coC}jeiY`SW zN&>eMxhO1(0U~l?SH%CG`hOP<{!e~!xnvl4yx~kDjs%hbLnNi7W#!}*6!$AZm0@sI zHFXV5t%KT!baeIf4UbqJvqRcDo^Wz;bI0L{6z`C*h^XioWIqeGP>a`mMcZ%*7 z-!HGMs;T2WYG~v)HNR}{=o0h}3=R#CzMFbK_v!QE_m%Y@jjTPH07`d z{&cy(_>k1}IZA5!v9q|Vex=L0&N*qXuIN1I0cW>#pHfzPP=^hWGesIb^#1FQ_T$&R zU&zGd*+ij$~L<=kFW)=SOjrs6QSX z2mZK_k~KF4bEL-Ne#PW?9XL4L<6x0YX)>?$u9qxMqa+xId-HAuz!}9lhOR^LY=ItI z5Xcrl*#Z(3ci@6x`nMrE|k{P7~Ku^`CdOEKQ)2drbmN7oSniUQQO z&2)d~-S4au5@l=JtYPW8DftCxaN`QE%*vy@l}+|Q?Jw| gCi2}MRGSUa$q>U(&CSpX;1d90oN?%yfB60OFCnXS-T(jq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..00225c9a2c5800ee8cc3ad4a123811f33b7f975e GIT binary patch literal 50019 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%0D731?v8faG#q))U_N1H8!D-i( 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%0DB)*GP*zwyj4yv)M9;>dco3RUK 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 literal 0 HcmV?d00001 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