From b003775553a0b51cbbd8bd25a5b9c4a27f46550c Mon Sep 17 00:00:00 2001 From: jdduke Date: Tue, 14 Apr 2015 18:55:08 -0700 Subject: [PATCH] [Android] Use the platform SwipeRefreshLayout Adopt the compat library's implementation of the pull-to-refresh effect, removing the old compositor-based approach. This depends directly on the SwipeRefreshLayout addition in https://codereview.chromium.org/897663003/. BUG=428429,444134 Review URL: https://codereview.chromium.org/894193005 Cr-Commit-Position: refs/heads/master@{#325172} --- chrome/android/BUILD.gn | 1 + .../chrome/browser/SwipeRefreshHandler.java | 164 ++++++ .../java/src/org/chromium/chrome/browser/Tab.java | 13 + .../java/strings/android_chrome_strings.grd | 7 +- chrome/chrome.gyp | 1 + content/browser/android/content_view_core_impl.cc | 31 ++ content/browser/android/content_view_core_impl.h | 8 + .../android/overscroll_controller_android.cc | 88 +-- .../android/overscroll_controller_android.h | 16 +- content/browser/android/overscroll_glow.cc | 2 +- content/browser/android/overscroll_glow.h | 6 +- content/browser/android/overscroll_refresh.cc | 618 ++++----------------- content/browser/android/overscroll_refresh.h | 73 +-- .../browser/android/overscroll_refresh_unittest.cc | 555 ++++++++---------- .../render_widget_host_view_android.cc | 12 +- .../chromium/content/browser/ContentViewCore.java | 38 ++ .../content/browser/OverscrollRefreshHandler.java | 40 ++ third_party/android_swipe_refresh/BUILD.gn | 3 - third_party/android_swipe_refresh/README.chromium | 11 +- .../android_swipe_refresh.gyp | 3 - .../android/swiperefresh/CircleImageView.java | 45 +- .../swiperefresh/MaterialProgressDrawable.java | 31 +- .../android/swiperefresh/SwipeRefreshLayout.java | 476 ++++++---------- ui/android/java/res/drawable-hdpi/refresh_blue.png | Bin 1436 -> 0 bytes ui/android/java/res/drawable-hdpi/refresh_gray.png | Bin 1208 -> 0 bytes ui/android/java/res/drawable-mdpi/refresh_blue.png | Bin 939 -> 0 bytes ui/android/java/res/drawable-mdpi/refresh_gray.png | Bin 757 -> 0 bytes .../java/res/drawable-xhdpi/refresh_blue.png | Bin 1941 -> 0 bytes .../java/res/drawable-xhdpi/refresh_gray.png | Bin 1683 -> 0 bytes .../java/res/drawable-xxhdpi/refresh_blue.png | Bin 2965 -> 0 bytes .../java/res/drawable-xxhdpi/refresh_gray.png | Bin 2633 -> 0 bytes .../java/res/drawable-xxxhdpi/refresh_blue.png | Bin 3834 -> 0 bytes .../java/res/drawable-xxxhdpi/refresh_gray.png | Bin 3446 -> 0 bytes .../ui/resources/system/SystemResourceLoader.java | 7 - ui/android/resources/system_ui_resource_type.h | 2 - 35 files changed, 930 insertions(+), 1321 deletions(-) create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/SwipeRefreshHandler.java rewrite content/browser/android/overscroll_refresh.cc (85%) rewrite content/browser/android/overscroll_refresh_unittest.cc (83%) create mode 100644 content/public/android/java/src/org/chromium/content/browser/OverscrollRefreshHandler.java rename third_party/android_swipe_refresh/java/src/{ => org}/chromium/third_party/android/swiperefresh/CircleImageView.java (79%) rename third_party/android_swipe_refresh/java/src/{ => org}/chromium/third_party/android/swiperefresh/MaterialProgressDrawable.java (96%) rename third_party/android_swipe_refresh/java/src/{ => org}/chromium/third_party/android/swiperefresh/SwipeRefreshLayout.java (61%) delete mode 100644 ui/android/java/res/drawable-hdpi/refresh_blue.png delete mode 100644 ui/android/java/res/drawable-hdpi/refresh_gray.png delete mode 100644 ui/android/java/res/drawable-mdpi/refresh_blue.png delete mode 100644 ui/android/java/res/drawable-mdpi/refresh_gray.png delete mode 100644 ui/android/java/res/drawable-xhdpi/refresh_blue.png delete mode 100644 ui/android/java/res/drawable-xhdpi/refresh_gray.png delete mode 100644 ui/android/java/res/drawable-xxhdpi/refresh_blue.png delete mode 100644 ui/android/java/res/drawable-xxhdpi/refresh_gray.png delete mode 100644 ui/android/java/res/drawable-xxxhdpi/refresh_blue.png delete mode 100644 ui/android/java/res/drawable-xxxhdpi/refresh_gray.png diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn index 936557e5ceb4..14e633515e24 100644 --- a/chrome/android/BUILD.gn +++ b/chrome/android/BUILD.gn @@ -98,6 +98,7 @@ android_library("chrome_java") { "//third_party/android_media:android_media_java", "//third_party/android_protobuf:protobuf_nano_javalib", "//third_party/android_tools:android_gcm_java", + "//third_party/android_swipe_refresh:android_swipe_refresh_java", "//third_party/android_tools:android_support_v13_java", "//third_party/android_tools:android_support_v7_appcompat_java", "//third_party/android_tools:android_support_v7_mediarouter_java", diff --git a/chrome/android/java/src/org/chromium/chrome/browser/SwipeRefreshHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/SwipeRefreshHandler.java new file mode 100644 index 000000000000..26dcb5a0b7a5 --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/SwipeRefreshHandler.java @@ -0,0 +1,164 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser; + +import android.content.Context; +import android.view.ViewGroup.LayoutParams; + +import org.chromium.base.JNINamespace; +import org.chromium.base.TraceEvent; +import org.chromium.chrome.R; +import org.chromium.content.browser.ContentViewCore; +import org.chromium.content.browser.OverscrollRefreshHandler; +import org.chromium.third_party.android.swiperefresh.SwipeRefreshLayout; + +/** + * An overscroll handler implemented in terms a modified version of the Android + * compat library's SwipeRefreshLayout effect. + */ +@JNINamespace("content") +public class SwipeRefreshHandler implements OverscrollRefreshHandler { + // Synthetic delay between the {@link #didStopRefreshing()} signal and the + // call to stop the refresh animation. + private static final int STOP_REFRESH_ANIMATION_DELAY_MS = 500; + + // The modified AppCompat version of the refresh effect, handling all core + // logic, rendering and animation. + private final SwipeRefreshLayout mSwipeRefreshLayout; + + // The ContentViewCore with which the handler is associated. The handler + // will set/unset itself as the default OverscrollRefreshHandler as the + // association changes. + private ContentViewCore mContentViewCore; + + // Async runnable for ending the refresh animation after the page first + // loads a frame. This is used to provide a reasonable minimum animation time. + private Runnable mStopRefreshingRunnable; + + // Accessibility utterance used to indicate refresh activation. + private String mAccessibilityRefreshString; + + /** + * Simple constructor to use when creating an OverscrollRefresh instance from code. + * + * @param context The associated context. + */ + public SwipeRefreshHandler(Context context) { + mSwipeRefreshLayout = new SwipeRefreshLayout(context); + mSwipeRefreshLayout.setLayoutParams( + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + mSwipeRefreshLayout.setColorSchemeResources(R.color.light_active_color); + // SwipeRefreshLayout.LARGE layouts appear broken on JellyBean. + mSwipeRefreshLayout.setSize(SwipeRefreshLayout.DEFAULT); + mSwipeRefreshLayout.setEnabled(false); + } + + /** + * Pair the effect with a given ContentViewCore instance. If that instance is null, + * the effect will be disabled. + * @param contentViewCore The associated ContentViewCore instance. + */ + public void setContentViewCore(final ContentViewCore contentViewCore) { + if (mContentViewCore == contentViewCore) return; + + if (mContentViewCore != null) { + setEnabled(false); + mSwipeRefreshLayout.setOnRefreshListener(null); + mContentViewCore.setOverscrollRefreshHandler(null); + } + + mContentViewCore = contentViewCore; + + if (mContentViewCore == null) return; + + setEnabled(true); + mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + if (mStopRefreshingRunnable != null) { + mSwipeRefreshLayout.removeCallbacks(mStopRefreshingRunnable); + } + if (mAccessibilityRefreshString == null) { + int resId = R.string.accessibility_swipe_refresh; + mAccessibilityRefreshString = + contentViewCore.getContext().getResources().getString(resId); + } + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + mSwipeRefreshLayout.announceForAccessibility(mAccessibilityRefreshString); + } + contentViewCore.getWebContents().getNavigationController().reload(true); + } + }); + contentViewCore.setOverscrollRefreshHandler(this); + } + + /** + * Notify the SwipeRefreshLayout that a refresh action has completed. + * Defer the notification by a reasonable minimum to ensure sufficient + * visiblity of the animation. + */ + public void didStopRefreshing() { + if (!mSwipeRefreshLayout.isRefreshing()) return; + if (mStopRefreshingRunnable == null) { + mStopRefreshingRunnable = new Runnable() { + @Override + public void run() { + mSwipeRefreshLayout.setRefreshing(false); + } + }; + } + mSwipeRefreshLayout.removeCallbacks(mStopRefreshingRunnable); + mSwipeRefreshLayout.postDelayed(mStopRefreshingRunnable, STOP_REFRESH_ANIMATION_DELAY_MS); + } + + @Override + public boolean start() { + attachSwipeRefreshLayoutIfNecessary(); + return mSwipeRefreshLayout.start(); + } + + @Override + public void pull(float delta) { + TraceEvent.begin("SwipeRefreshHandler.pull"); + mSwipeRefreshLayout.pull(delta); + TraceEvent.end("SwipeRefreshHandler.pull"); + } + + @Override + public void release(boolean allowRefresh) { + TraceEvent.begin("SwipeRefreshHandler.release"); + mSwipeRefreshLayout.release(allowRefresh); + TraceEvent.end("SwipeRefreshHandler.release"); + } + + @Override + public void reset() { + mSwipeRefreshLayout.reset(); + detachSwipeRefreshLayoutIfNecessary(); + } + + @Override + public void setEnabled(boolean enabled) { + mSwipeRefreshLayout.setEnabled(enabled); + if (!enabled) reset(); + } + + // The animation view is attached/detached on-demand to minimize overlap + // with composited SurfaceView content. + private void attachSwipeRefreshLayoutIfNecessary() { + if (mContentViewCore == null) return; + if (mSwipeRefreshLayout.getParent() == null) { + mContentViewCore.getContainerView().addView(mSwipeRefreshLayout); + } + } + + private void detachSwipeRefreshLayoutIfNecessary() { + // TODO(jdduke): Also detach the effect when its animation ends. + if (mContentViewCore == null) return; + if (mSwipeRefreshLayout.getParent() != null) { + mContentViewCore.getContainerView().removeView(mSwipeRefreshLayout); + } + } +} diff --git a/chrome/android/java/src/org/chromium/chrome/browser/Tab.java b/chrome/android/java/src/org/chromium/chrome/browser/Tab.java index d4f16edbde7c..1cec7a3dfc01 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/Tab.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/Tab.java @@ -156,6 +156,9 @@ public class Tab implements ViewGroup.OnHierarchyChangeListener, /** Manages app banners shown for this tab. */ private AppBannerManager mAppBannerManager; + /** Controls overscroll pull-to-refresh behavior for this tab. */ + private SwipeRefreshHandler mSwipeRefreshHandler; + /** The sync id of the Tab if session sync is enabled. */ private int mSyncId; @@ -633,7 +636,10 @@ public class Tab implements ViewGroup.OnHierarchyChangeListener, observer.onDidNavigateMainFrame( Tab.this, url, baseUrl, isNavigationToDifferentPage, isFragmentNavigation, statusCode); + } + if (mSwipeRefreshHandler != null) { + mSwipeRefreshHandler.didStopRefreshing(); } } @@ -1603,6 +1609,9 @@ public class Tab implements ViewGroup.OnHierarchyChangeListener, } mInfoBarContainer.setContentViewCore(mContentViewCore); + mSwipeRefreshHandler = new SwipeRefreshHandler(mContext); + mSwipeRefreshHandler.setContentViewCore(mContentViewCore); + if (DomDistillerFeedbackReporter.isEnabled() && mDomDistillerFeedbackReporter == null) { mDomDistillerFeedbackReporter = new DomDistillerFeedbackReporter(this); } @@ -2040,6 +2049,10 @@ public class Tab implements ViewGroup.OnHierarchyChangeListener, mInfoBarContainer.removeFromParentView(); mInfoBarContainer.setContentViewCore(null); } + if (mSwipeRefreshHandler != null) { + mSwipeRefreshHandler.setContentViewCore(null); + mSwipeRefreshHandler = null; + } mContentViewParent = null; mContentViewCore.destroy(); mContentViewCore = null; diff --git a/chrome/android/java/strings/android_chrome_strings.grd b/chrome/android/java/strings/android_chrome_strings.grd index df872bc78e4a..8b1d1689e008 100644 --- a/chrome/android/java/strings/android_chrome_strings.grd +++ b/chrome/android/java/strings/android_chrome_strings.grd @@ -1162,7 +1162,12 @@ You are signing in with a managed account and giving its administrator control o Save video - + + + + + Refreshing page + diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp index 690be0462706..5137c93dd2af 100644 --- a/chrome/chrome.gyp +++ b/chrome/chrome.gyp @@ -629,6 +629,7 @@ '../sync/sync.gyp:sync_java', '../third_party/android_data_chart/android_data_chart.gyp:android_data_chart_java', '../third_party/android_media/android_media.gyp:android_media_java', + '../third_party/android_swipe_refresh/android_swipe_refresh.gyp:android_swipe_refresh_java', '../third_party/android_tools/android_tools.gyp:android_support_v7_appcompat_javalib', '../third_party/android_tools/android_tools.gyp:android_support_v7_mediarouter_javalib', '../third_party/android_tools/android_tools.gyp:android_support_v13_javalib', diff --git a/content/browser/android/content_view_core_impl.cc b/content/browser/android/content_view_core_impl.cc index a7bcaecdef1a..e87fd3d51f9a 100644 --- a/content/browser/android/content_view_core_impl.cc +++ b/content/browser/android/content_view_core_impl.cc @@ -1410,6 +1410,37 @@ void ContentViewCoreImpl::WebContentsDestroyed() { wcva->SetContentViewCore(NULL); } +bool ContentViewCoreImpl::PullStart() { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef obj = java_ref_.get(env); + if (!obj.is_null()) + return Java_ContentViewCore_onOverscrollRefreshStart(env, obj.obj()); + return false; +} + +void ContentViewCoreImpl::PullUpdate(float delta) { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef obj = java_ref_.get(env); + if (!obj.is_null()) + Java_ContentViewCore_onOverscrollRefreshUpdate(env, obj.obj(), delta); +} + +void ContentViewCoreImpl::PullRelease(bool allow_refresh) { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef obj = java_ref_.get(env); + if (!obj.is_null()) { + Java_ContentViewCore_onOverscrollRefreshRelease(env, obj.obj(), + allow_refresh); + } +} + +void ContentViewCoreImpl::PullReset() { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef obj = java_ref_.get(env); + if (!obj.is_null()) + Java_ContentViewCore_onOverscrollRefreshReset(env, obj.obj()); +} + // This is called for each ContentView. jlong Init(JNIEnv* env, jobject obj, diff --git a/content/browser/android/content_view_core_impl.h b/content/browser/android/content_view_core_impl.h index 284fb38bd69c..a287c65f3ddf 100644 --- a/content/browser/android/content_view_core_impl.h +++ b/content/browser/android/content_view_core_impl.h @@ -13,6 +13,7 @@ #include "base/i18n/rtl.h" #include "base/memory/scoped_ptr.h" #include "base/process/process.h" +#include "content/browser/android/overscroll_refresh.h" #include "content/browser/renderer_host/render_widget_host_view_android.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/public/browser/android/content_view_core.h" @@ -35,6 +36,7 @@ class RenderWidgetHostViewAndroid; struct MenuItem; class ContentViewCoreImpl : public ContentViewCore, + public OverscrollRefreshHandler, public WebContentsObserver { public: static ContentViewCoreImpl* FromWebContents(WebContents* web_contents); @@ -305,6 +307,12 @@ class ContentViewCoreImpl : public ContentViewCore, RenderViewHost* new_host) override; void WebContentsDestroyed() override; + // OverscrollRefreshHandler implementation. + bool PullStart() override; + void PullUpdate(float delta) override; + void PullRelease(bool allow_refresh) override; + void PullReset() override; + // -------------------------------------------------------------------------- // Other private methods and data // -------------------------------------------------------------------------- diff --git a/content/browser/android/overscroll_controller_android.cc b/content/browser/android/overscroll_controller_android.cc index 4548cf33ce20..02e2c01383eb 100644 --- a/content/browser/android/overscroll_controller_android.cc +++ b/content/browser/android/overscroll_controller_android.cc @@ -8,6 +8,7 @@ #include "base/command_line.h" #include "cc/layers/layer.h" #include "cc/output/compositor_frame_metadata.h" +#include "content/browser/android/content_view_core_impl.h" #include "content/browser/android/edge_effect.h" #include "content/browser/android/edge_effect_l.h" #include "content/browser/web_contents/web_contents_impl.h" @@ -17,6 +18,7 @@ #include "content/public/common/content_switches.h" #include "third_party/WebKit/public/web/WebInputEvent.h" #include "ui/android/resources/resource_manager.h" +#include "ui/android/window_android.h" #include "ui/android/window_android_compositor.h" #include "ui/base/l10n/l10n_util_android.h" @@ -26,10 +28,6 @@ namespace { // Used for conditional creation of EdgeEffect types for the overscroll glow. const int kAndroidLSDKVersion = 21; -// Default offset in dips from the top of the view beyond which the refresh -// action will be activated. -const int kDefaultRefreshDragTargetDips = 64; - // If the glow effect alpha is greater than this value, the refresh effect will // be suppressed. This value was experimentally determined to provide a // reasonable balance between avoiding accidental refresh activation and @@ -79,37 +77,26 @@ scoped_ptr CreateGlowEffect(OverscrollGlowClient* client, } scoped_ptr CreateRefreshEffect( - ui::WindowAndroidCompositor* compositor, - OverscrollRefreshClient* client, - float dpi_scale) { + OverscrollRefreshHandler* handler) { if (base::CommandLine::ForCurrentProcess()->HasSwitch( switches::kDisablePullToRefreshEffect)) { return nullptr; } - return make_scoped_ptr(new OverscrollRefresh( - &compositor->GetResourceManager(), client, - kDefaultRefreshDragTargetDips * dpi_scale, l10n_util::IsLayoutRtl())); + return make_scoped_ptr(new OverscrollRefresh(handler)); } } // namespace OverscrollControllerAndroid::OverscrollControllerAndroid( - WebContents* web_contents, - ui::WindowAndroidCompositor* compositor, - float dpi_scale) - : compositor_(compositor), - dpi_scale_(dpi_scale), + ContentViewCoreImpl* content_view_core) + : compositor_(content_view_core->GetWindowAndroid()->GetCompositor()), + dpi_scale_(content_view_core->GetDpiScale()), enabled_(true), - glow_effect_(CreateGlowEffect(this, dpi_scale)), - refresh_effect_(CreateRefreshEffect(compositor, this, dpi_scale)), - triggered_refresh_active_(false), - is_fullscreen_(static_cast(web_contents) - ->IsFullscreenForCurrentTab()) { - DCHECK(web_contents); - DCHECK(compositor); - if (refresh_effect_) - Observe(web_contents); + glow_effect_(CreateGlowEffect(this, dpi_scale_)), + refresh_effect_(CreateRefreshEffect(content_view_core)), + is_fullscreen_(false) { + DCHECK(compositor_); } OverscrollControllerAndroid::~OverscrollControllerAndroid() { @@ -134,7 +121,6 @@ bool OverscrollControllerAndroid::WillHandleGestureEvent( } bool handled = false; - bool maybe_needs_animate = false; switch (event.type) { case blink::WebInputEvent::GestureScrollBegin: refresh_effect_->OnScrollBegin(); @@ -144,21 +130,19 @@ bool OverscrollControllerAndroid::WillHandleGestureEvent( gfx::Vector2dF scroll_delta(event.data.scrollUpdate.deltaX, event.data.scrollUpdate.deltaY); scroll_delta.Scale(dpi_scale_); - maybe_needs_animate = true; handled = refresh_effect_->WillHandleScrollUpdate(scroll_delta); } break; case blink::WebInputEvent::GestureScrollEnd: refresh_effect_->OnScrollEnd(gfx::Vector2dF()); - maybe_needs_animate = true; break; case blink::WebInputEvent::GestureFlingStart: { - gfx::Vector2dF scroll_velocity(event.data.flingStart.velocityX, - event.data.flingStart.velocityY); - scroll_velocity.Scale(dpi_scale_); - refresh_effect_->OnScrollEnd(scroll_velocity); if (refresh_effect_->IsActive()) { + gfx::Vector2dF scroll_velocity(event.data.flingStart.velocityX, + event.data.flingStart.velocityY); + scroll_velocity.Scale(dpi_scale_); + refresh_effect_->OnScrollEnd(scroll_velocity); // TODO(jdduke): Figure out a cleaner way of suppressing a fling. // It's important that the any downstream code sees a scroll-ending // event (in this case GestureFlingStart) if it has seen a scroll begin. @@ -171,7 +155,6 @@ bool OverscrollControllerAndroid::WillHandleGestureEvent( modified_event.data.flingStart.velocityX = .01f; modified_event.data.flingStart.velocityY = .01f; } - maybe_needs_animate = true; } break; case blink::WebInputEvent::GesturePinchBegin: @@ -182,9 +165,6 @@ bool OverscrollControllerAndroid::WillHandleGestureEvent( break; } - if (maybe_needs_animate && refresh_effect_->IsActive()) - SetNeedsAnimate(); - return handled; } @@ -242,12 +222,7 @@ bool OverscrollControllerAndroid::Animate(base::TimeTicks current_time, if (!enabled_) return false; - bool needs_animate = false; - if (refresh_effect_) - needs_animate |= refresh_effect_->Animate(current_time, parent_layer); - if (glow_effect_) - needs_animate |= glow_effect_->Animate(current_time, parent_layer); - return needs_animate; + return glow_effect_->Animate(current_time, parent_layer); } void OverscrollControllerAndroid::OnFrameMetadataUpdated( @@ -265,13 +240,13 @@ void OverscrollControllerAndroid::OnFrameMetadataUpdated( gfx::ScaleVector2d(frame_metadata.root_scroll_offset, scale_factor); if (refresh_effect_) { - refresh_effect_->UpdateDisplay(viewport_size, content_scroll_offset, - frame_metadata.root_overflow_y_hidden); + refresh_effect_->OnFrameUpdated(content_scroll_offset, + frame_metadata.root_overflow_y_hidden); } if (glow_effect_) { - glow_effect_->UpdateDisplay(viewport_size, content_size, - content_scroll_offset); + glow_effect_->OnFrameUpdated(viewport_size, content_size, + content_scroll_offset); } } @@ -291,15 +266,6 @@ void OverscrollControllerAndroid::Disable() { } } -void OverscrollControllerAndroid::DidNavigateMainFrame( - const LoadCommittedDetails& details, - const FrameNavigateParams& params) { - // Once the main frame has navigated, there's little need to further animate - // the reload effect. Note that the effect will naturally time out should the - // reload be interruped for any reason. - triggered_refresh_active_ = false; -} - void OverscrollControllerAndroid::DidToggleFullscreenModeForTab( bool entered_fullscreen) { if (is_fullscreen_ == entered_fullscreen) @@ -309,20 +275,6 @@ void OverscrollControllerAndroid::DidToggleFullscreenModeForTab( refresh_effect_->ReleaseWithoutActivation(); } -void OverscrollControllerAndroid::TriggerRefresh() { - triggered_refresh_active_ = false; - if (!web_contents()) - return; - - triggered_refresh_active_ = true; - RecordAction(base::UserMetricsAction("MobilePullGestureReload")); - web_contents()->GetController().Reload(true); -} - -bool OverscrollControllerAndroid::IsStillRefreshing() const { - return triggered_refresh_active_; -} - scoped_ptr OverscrollControllerAndroid::CreateEdgeEffect() { return CreateGlowEdgeEffect(&compositor_->GetResourceManager(), dpi_scale_); } diff --git a/content/browser/android/overscroll_controller_android.h b/content/browser/android/overscroll_controller_android.h index e7a5196bda64..5f76dab43527 100644 --- a/content/browser/android/overscroll_controller_android.h +++ b/content/browser/android/overscroll_controller_android.h @@ -27,18 +27,16 @@ class WindowAndroidCompositor; namespace content { +class ContentViewCoreImpl; struct DidOverscrollParams; // Glue class for handling all inputs into Android-specific overscroll effects, // both the passive overscroll glow and the active overscroll pull-to-refresh. // Note that all input coordinates (both for events and overscroll) are in DIPs. class OverscrollControllerAndroid : public OverscrollGlowClient, - public OverscrollRefreshClient, public WebContentsObserver { public: - OverscrollControllerAndroid(WebContents* web_contents, - ui::WindowAndroidCompositor* compositor, - float dpi_scale); + explicit OverscrollControllerAndroid(ContentViewCoreImpl* content_view_core); ~OverscrollControllerAndroid() override; // Returns true if |event| is consumed by an overscroll effect, in which @@ -66,20 +64,14 @@ class OverscrollControllerAndroid : public OverscrollGlowClient, private: // WebContentsObserver implementation. - void DidNavigateMainFrame(const LoadCommittedDetails& details, - const FrameNavigateParams& params) override; void DidToggleFullscreenModeForTab(bool entered_fullscreen) override; - // OverscrollRefreshClient implementation. - void TriggerRefresh() override; - bool IsStillRefreshing() const override; - // OverscrollGlowClient implementation. scoped_ptr CreateEdgeEffect() override; void SetNeedsAnimate(); - ui::WindowAndroidCompositor* compositor_; + ui::WindowAndroidCompositor* const compositor_; const float dpi_scale_; bool enabled_; @@ -87,7 +79,7 @@ class OverscrollControllerAndroid : public OverscrollGlowClient, // TODO(jdduke): Factor out a common API from the two overscroll effects. scoped_ptr glow_effect_; scoped_ptr refresh_effect_; - bool triggered_refresh_active_; + bool has_initialized_refresh_effect_; bool is_fullscreen_; DISALLOW_COPY_AND_ASSIGN(OverscrollControllerAndroid); diff --git a/content/browser/android/overscroll_glow.cc b/content/browser/android/overscroll_glow.cc index f7eb4bb9b0d9..61ffc08b3ae2 100644 --- a/content/browser/android/overscroll_glow.cc +++ b/content/browser/android/overscroll_glow.cc @@ -167,7 +167,7 @@ bool OverscrollGlow::Animate(base::TimeTicks current_time, return CheckNeedsAnimate(); } -void OverscrollGlow::UpdateDisplay( +void OverscrollGlow::OnFrameUpdated( const gfx::SizeF& viewport_size, const gfx::SizeF& content_size, const gfx::Vector2dF& content_scroll_offset) { diff --git a/content/browser/android/overscroll_glow.h b/content/browser/android/overscroll_glow.h index 80a1b106cf4a..6ae45dde4aca 100644 --- a/content/browser/android/overscroll_glow.h +++ b/content/browser/android/overscroll_glow.h @@ -59,9 +59,9 @@ class OverscrollGlow { // Update the effect according to the most recent display parameters, // Note: All dimensions are in device pixels. - void UpdateDisplay(const gfx::SizeF& viewport_size, - const gfx::SizeF& content_size, - const gfx::Vector2dF& content_scroll_offset); + void OnFrameUpdated(const gfx::SizeF& viewport_size, + const gfx::SizeF& content_size, + const gfx::Vector2dF& content_scroll_offset); // Reset the effect to its inactive state, clearing any active effects. void Reset(); diff --git a/content/browser/android/overscroll_refresh.cc b/content/browser/android/overscroll_refresh.cc dissimilarity index 85% index 58962f6356ec..c2a1453bc175 100644 --- a/content/browser/android/overscroll_refresh.cc +++ b/content/browser/android/overscroll_refresh.cc @@ -1,514 +1,104 @@ -// Copyright 2014 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "content/browser/android/overscroll_refresh.h" - -#include "cc/animation/timing_function.h" -#include "cc/layers/ui_resource_layer.h" -#include "cc/trees/layer_tree_host.h" -#include "content/browser/android/animation_utils.h" -#include "ui/android/resources/resource_manager.h" -#include "ui/android/resources/system_ui_resource_type.h" -#include "ui/gfx/geometry/size_conversions.h" - -using std::abs; -using std::max; -using std::min; - -namespace content { -namespace { - -const ui::SystemUIResourceType kIdleResourceId = ui::OVERSCROLL_REFRESH_IDLE; -const ui::SystemUIResourceType kActiveResourceId = - ui::OVERSCROLL_REFRESH_ACTIVE; - -// Drag movement multiplier between user input and effect translation. -const float kDragRate = .5f; - -// Animation duration after the effect is released without triggering a refresh. -const int kRecedeTimeMs = 300; - -// Animation duration immediately after the effect is released and activated. -const int kActivationStartTimeMs = 150; - -// Animation duration after the effect is released and triggers a refresh. -const int kActivationTimeMs = 850; - -// Max animation duration after the effect is released and triggers a refresh. -const int kMaxActivationTimeMs = kActivationTimeMs * 4; - -// Animation duration after the refresh activated phase has completed. -const int kActivationRecedeTimeMs = 250; - -// Input threshold required to start glowing. -const float kGlowActivationThreshold = 0.85f; - -// Minimum alpha for the effect layer. -const float kMinAlpha = 0.3f; - -// Experimentally determined constant used to allow activation even if touch -// release results in a small upward fling (quite common during a slow scroll). -const float kMinFlingVelocityForActivation = -500.f; - -const float kEpsilon = 0.005f; - -void UpdateLayer(cc::UIResourceLayer* layer, - cc::Layer* parent, - cc::UIResourceId res_id, - const gfx::Size& size, - const gfx::Vector2dF& offset, - float opacity, - float rotation, - bool mirror) { - DCHECK(layer); - DCHECK(parent); - DCHECK(parent->layer_tree_host()); - if (layer->parent() != parent) - parent->AddChild(layer); - - if (size.IsEmpty()) { - layer->SetIsDrawable(false); - return; - } - - if (!res_id) { - layer->SetIsDrawable(false); - return; - } - - if (opacity == 0) { - layer->SetIsDrawable(false); - layer->SetOpacity(0); - return; - } - - layer->SetUIResourceId(res_id); - layer->SetIsDrawable(true); - layer->SetTransformOrigin( - gfx::Point3F(size.width() * 0.5f, size.height() * 0.5f, 0)); - layer->SetBounds(size); - layer->SetContentsOpaque(false); - layer->SetOpacity(Clamp(opacity, 0.f, 1.f)); - - float offset_x = offset.x() - size.width() * 0.5f; - float offset_y = offset.y() - size.height() * 0.5f; - gfx::Transform transform; - transform.Translate(offset_x, offset_y); - if (mirror) - transform.Scale(-1.f, 1.f); - transform.Rotate(rotation); - layer->SetTransform(transform); -} - -} // namespace - -class OverscrollRefresh::Effect { - public: - Effect(ui::ResourceManager* resource_manager, float target_drag, bool mirror) - : resource_manager_(resource_manager), - idle_layer_(cc::UIResourceLayer::Create()), - active_layer_(cc::UIResourceLayer::Create()), - target_drag_(target_drag), - mirror_(mirror), - drag_(0), - idle_alpha_(0), - active_alpha_(0), - offset_(0), - rotation_(0), - size_scale_(1), - idle_alpha_start_(0), - idle_alpha_finish_(0), - active_alpha_start_(0), - active_alpha_finish_(0), - offset_start_(0), - offset_finish_(0), - rotation_start_(0), - rotation_finish_(0), - size_scale_start_(1), - size_scale_finish_(1), - state_(STATE_IDLE), - ease_out_(cc::EaseOutTimingFunction::Create()), - ease_in_out_(cc::EaseInOutTimingFunction::Create()) { - DCHECK(target_drag_); - idle_layer_->SetIsDrawable(false); - active_layer_->SetIsDrawable(false); - } - - ~Effect() { Detach(); } - - void Pull(float delta) { - if (state_ != STATE_PULL) - drag_ = 0; - - state_ = STATE_PULL; - - delta *= kDragRate; - float max_delta = target_drag_ / OverscrollRefresh::kMinPullsToActivate; - delta = Clamp(delta, -max_delta, max_delta); - - drag_ += delta; - drag_ = Clamp(drag_, 0.f, target_drag_ * 3.f); - - // The following logic and constants were taken from Android's refresh - // effect (see SwipeRefreshLayout.java from v4 of the AppCompat library). - float original_drag_percent = drag_ / target_drag_; - float drag_percent = min(1.f, abs(original_drag_percent)); - float adjusted_percent = max(drag_percent - .4f, 0.f) * 5.f / 3.f; - float extra_os = abs(drag_) - target_drag_; - float slingshot_dist = target_drag_; - float tension_slingshot_percent = - max(0.f, min(extra_os, slingshot_dist * 2) / slingshot_dist); - float tension_percent = ((tension_slingshot_percent / 4) - - std::pow((tension_slingshot_percent / 4), 2.f)) * - 2.f; - float extra_move = slingshot_dist * tension_percent * 2; - - offset_ = slingshot_dist * drag_percent + extra_move; - - rotation_ = - 360.f * ((-0.25f + .4f * adjusted_percent + tension_percent * 2) * .5f); - - idle_alpha_ = - kMinAlpha + (1.f - kMinAlpha) * drag_percent / kGlowActivationThreshold; - active_alpha_ = (drag_percent - kGlowActivationThreshold) / - (1.f - kGlowActivationThreshold); - idle_alpha_ = Clamp(idle_alpha_, 0.f, 1.f); - active_alpha_ = Clamp(active_alpha_, 0.f, 1.f); - - size_scale_ = 1; - } - - bool Animate(base::TimeTicks current_time, bool still_refreshing) { - if (IsFinished()) - return false; - - if (state_ == STATE_PULL) - return true; - - const double dt = (current_time - start_time_).InMilliseconds(); - const double t = dt / duration_.InMilliseconds(); - const float interp = ease_out_->GetValue(min(t, 1.)); - - idle_alpha_ = Lerp(idle_alpha_start_, idle_alpha_finish_, interp); - active_alpha_ = Lerp(active_alpha_start_, active_alpha_finish_, interp); - offset_ = Lerp(offset_start_, offset_finish_, interp); - size_scale_ = Lerp(size_scale_start_, size_scale_finish_, interp); - - if (state_ == STATE_ACTIVATED || state_ == STATE_ACTIVATED_RECEDE) { - float adjusted_interp = ease_in_out_->GetValue(min(t, 1.)); - rotation_ = Lerp(rotation_start_, rotation_finish_, adjusted_interp); - // Add a small constant rotational velocity during activation. - rotation_ += dt * 90.f / kActivationTimeMs; - } else { - rotation_ = Lerp(rotation_start_, rotation_finish_, interp); - } - - if (t < 1.f - kEpsilon) - return true; - - switch (state_) { - case STATE_IDLE: - case STATE_PULL: - NOTREACHED() << "Invalidate state for animation."; - break; - case STATE_ACTIVATED_START: - // Briefly pause the animation after the rapid initial translation. - if (t < 1.5f) - break; - state_ = STATE_ACTIVATED; - start_time_ = current_time; - duration_ = base::TimeDelta::FromMilliseconds(kActivationTimeMs); - activated_start_time_ = current_time; - offset_start_ = offset_finish_ = offset_; - rotation_start_ = rotation_; - rotation_finish_ = rotation_start_ + 270.f; - size_scale_start_ = size_scale_finish_ = size_scale_; - break; - case STATE_ACTIVATED: - start_time_ = current_time; - if (still_refreshing && - (current_time - activated_start_time_ < - base::TimeDelta::FromMilliseconds(kMaxActivationTimeMs))) { - offset_start_ = offset_finish_ = offset_; - rotation_start_ = rotation_; - rotation_finish_ = rotation_start_ + 270.f; - break; - } - state_ = STATE_ACTIVATED_RECEDE; - duration_ = base::TimeDelta::FromMilliseconds(kActivationRecedeTimeMs); - rotation_start_ = rotation_finish_ = rotation_; - offset_start_ = offset_finish_ = offset_; - size_scale_start_ = size_scale_; - size_scale_finish_ = 0; - break; - case STATE_ACTIVATED_RECEDE: - Finish(); - break; - case STATE_RECEDE: - Finish(); - break; - }; - - return !IsFinished(); - } - - bool Release(base::TimeTicks current_time, bool allow_activation) { - switch (state_) { - case STATE_PULL: - break; - - case STATE_ACTIVATED: - case STATE_ACTIVATED_START: - // Avoid redundant activations. - if (allow_activation) - return false; - break; - - case STATE_IDLE: - case STATE_ACTIVATED_RECEDE: - case STATE_RECEDE: - // These states have already been "released" in some fashion. - return false; - } - - start_time_ = current_time; - idle_alpha_start_ = idle_alpha_; - active_alpha_start_ = active_alpha_; - offset_start_ = offset_; - rotation_start_ = rotation_; - size_scale_start_ = size_scale_finish_ = size_scale_; - - if (drag_ < target_drag_ || !allow_activation) { - state_ = STATE_RECEDE; - duration_ = base::TimeDelta::FromMilliseconds(kRecedeTimeMs); - idle_alpha_finish_ = 0; - active_alpha_finish_ = 0; - offset_finish_ = 0; - rotation_finish_ = rotation_start_ - 180.f; - return false; - } - - state_ = STATE_ACTIVATED_START; - duration_ = base::TimeDelta::FromMilliseconds(kActivationStartTimeMs); - activated_start_time_ = current_time; - idle_alpha_finish_ = idle_alpha_start_; - active_alpha_finish_ = active_alpha_start_; - offset_finish_ = target_drag_; - rotation_finish_ = rotation_start_; - return true; - } - - void Finish() { - Detach(); - idle_layer_->SetIsDrawable(false); - active_layer_->SetIsDrawable(false); - offset_ = 0; - idle_alpha_ = 0; - active_alpha_ = 0; - rotation_ = 0; - size_scale_ = 1; - state_ = STATE_IDLE; - } - - void ApplyToLayers(const gfx::SizeF& viewport_size, cc::Layer* parent) { - if (IsFinished()) - return; - - if (!parent->layer_tree_host()) - return; - - // An empty window size, while meaningless, is also relatively harmless, and - // will simply prevent any drawing of the layers. - if (viewport_size.IsEmpty()) { - idle_layer_->SetIsDrawable(false); - active_layer_->SetIsDrawable(false); - return; - } - - cc::UIResourceId idle_resource = resource_manager_->GetUIResourceId( - ui::ANDROID_RESOURCE_TYPE_SYSTEM, kIdleResourceId); - cc::UIResourceId active_resource = resource_manager_->GetUIResourceId( - ui::ANDROID_RESOURCE_TYPE_SYSTEM, kActiveResourceId); - - gfx::Size idle_size = - parent->layer_tree_host()->GetUIResourceSize(idle_resource); - gfx::Size active_size = - parent->layer_tree_host()->GetUIResourceSize(active_resource); - gfx::Size scaled_idle_size = - gfx::ToRoundedSize(gfx::ScaleSize(idle_size, size_scale_)); - gfx::Size scaled_active_size = - gfx::ToRoundedSize(gfx::ScaleSize(active_size, size_scale_)); - - gfx::Vector2dF idle_offset(viewport_size.width() * 0.5f, - offset_ - idle_size.height() * 0.5f); - gfx::Vector2dF active_offset(viewport_size.width() * 0.5f, - offset_ - active_size.height() * 0.5f); - - UpdateLayer(idle_layer_.get(), parent, idle_resource, scaled_idle_size, - idle_offset, idle_alpha_, rotation_, mirror_); - UpdateLayer(active_layer_.get(), parent, active_resource, - scaled_active_size, active_offset, active_alpha_, rotation_, - mirror_); - } - - bool IsFinished() const { return state_ == STATE_IDLE; } - - private: - enum State { - STATE_IDLE = 0, - STATE_PULL, - STATE_ACTIVATED_START, - STATE_ACTIVATED, - STATE_ACTIVATED_RECEDE, - STATE_RECEDE - }; - - void Detach() { - idle_layer_->RemoveFromParent(); - active_layer_->RemoveFromParent(); - } - - ui::ResourceManager* const resource_manager_; - - scoped_refptr idle_layer_; - scoped_refptr active_layer_; - - const float target_drag_; - const bool mirror_; - float drag_; - float idle_alpha_; - float active_alpha_; - float offset_; - float rotation_; - float size_scale_; - - float idle_alpha_start_; - float idle_alpha_finish_; - float active_alpha_start_; - float active_alpha_finish_; - float offset_start_; - float offset_finish_; - float rotation_start_; - float rotation_finish_; - float size_scale_start_; - float size_scale_finish_; - - base::TimeTicks start_time_; - base::TimeTicks activated_start_time_; - base::TimeDelta duration_; - - State state_; - - scoped_ptr ease_out_; - scoped_ptr ease_in_out_; - - DISALLOW_COPY_AND_ASSIGN(Effect); -}; - -OverscrollRefresh::OverscrollRefresh(ui::ResourceManager* resource_manager, - OverscrollRefreshClient* client, - float target_drag_offset_pixels, - bool mirror) - : client_(client), - scrolled_to_top_(true), - overflow_y_hidden_(false), - scroll_consumption_state_(DISABLED), - effect_(new Effect(resource_manager, target_drag_offset_pixels, mirror)) { - DCHECK(client); -} - -OverscrollRefresh::~OverscrollRefresh() { -} - -void OverscrollRefresh::Reset() { - scroll_consumption_state_ = DISABLED; - effect_->Finish(); -} - -void OverscrollRefresh::OnScrollBegin() { - ReleaseWithoutActivation(); - if (scrolled_to_top_ && !overflow_y_hidden_) - scroll_consumption_state_ = AWAITING_SCROLL_UPDATE_ACK; -} - -void OverscrollRefresh::OnScrollEnd(const gfx::Vector2dF& scroll_velocity) { - bool allow_activation = scroll_velocity.y() > kMinFlingVelocityForActivation; - Release(allow_activation); -} - -void OverscrollRefresh::OnScrollUpdateAck(bool was_consumed) { - if (scroll_consumption_state_ != AWAITING_SCROLL_UPDATE_ACK) - return; - - scroll_consumption_state_ = was_consumed ? DISABLED : ENABLED; -} - -bool OverscrollRefresh::WillHandleScrollUpdate( - const gfx::Vector2dF& scroll_delta) { - if (viewport_size_.IsEmpty()) - return false; - - switch (scroll_consumption_state_) { - case DISABLED: - return false; - - case AWAITING_SCROLL_UPDATE_ACK: - // If the initial scroll motion is downward, never allow activation. - if (scroll_delta.y() <= 0) - scroll_consumption_state_ = DISABLED; - return false; - - case ENABLED: { - effect_->Pull(scroll_delta.y()); - return true; - } - } - - NOTREACHED() << "Invalid overscroll state: " << scroll_consumption_state_; - return false; -} - -void OverscrollRefresh::ReleaseWithoutActivation() { - bool allow_activation = false; - Release(allow_activation); -} - -bool OverscrollRefresh::Animate(base::TimeTicks current_time, - cc::Layer* parent_layer) { - DCHECK(parent_layer); - if (effect_->IsFinished()) - return false; - - if (effect_->Animate(current_time, client_->IsStillRefreshing())) - effect_->ApplyToLayers(viewport_size_, parent_layer); - - return !effect_->IsFinished(); -} - -bool OverscrollRefresh::IsActive() const { - return scroll_consumption_state_ == ENABLED || !effect_->IsFinished(); -} - -bool OverscrollRefresh::IsAwaitingScrollUpdateAck() const { - return scroll_consumption_state_ == AWAITING_SCROLL_UPDATE_ACK; -} - -void OverscrollRefresh::UpdateDisplay( - const gfx::SizeF& viewport_size, - const gfx::Vector2dF& content_scroll_offset, - bool root_overflow_y_hidden) { - viewport_size_ = viewport_size; - scrolled_to_top_ = content_scroll_offset.y() == 0; - overflow_y_hidden_ = root_overflow_y_hidden; -} - -void OverscrollRefresh::Release(bool allow_activation) { - if (scroll_consumption_state_ == ENABLED) { - if (effect_->Release(base::TimeTicks::Now(), allow_activation)) - client_->TriggerRefresh(); - } - scroll_consumption_state_ = DISABLED; -} - -} // namespace content +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/android/overscroll_refresh.h" + +#include "base/logging.h" + +namespace content { +namespace { + +// Experimentally determined constant used to allow activation even if touch +// release results in a small upward fling (quite common during a slow scroll). +const float kMinFlingVelocityForActivation = -500.f; + +} // namespace + +OverscrollRefresh::OverscrollRefresh(OverscrollRefreshHandler* handler) + : scrolled_to_top_(true), + overflow_y_hidden_(false), + scroll_consumption_state_(DISABLED), + handler_(handler) { + DCHECK(handler); +} + +OverscrollRefresh::~OverscrollRefresh() { +} + +void OverscrollRefresh::Reset() { + scroll_consumption_state_ = DISABLED; + handler_->PullReset(); +} + +void OverscrollRefresh::OnScrollBegin() { + ReleaseWithoutActivation(); + if (scrolled_to_top_ && !overflow_y_hidden_) + scroll_consumption_state_ = AWAITING_SCROLL_UPDATE_ACK; +} + +void OverscrollRefresh::OnScrollEnd(const gfx::Vector2dF& scroll_velocity) { + bool allow_activation = scroll_velocity.y() > kMinFlingVelocityForActivation; + Release(allow_activation); +} + +void OverscrollRefresh::OnScrollUpdateAck(bool was_consumed) { + if (scroll_consumption_state_ != AWAITING_SCROLL_UPDATE_ACK) + return; + + if (was_consumed) { + scroll_consumption_state_ = DISABLED; + return; + } + + scroll_consumption_state_ = handler_->PullStart() ? ENABLED : DISABLED; +} + +bool OverscrollRefresh::WillHandleScrollUpdate( + const gfx::Vector2dF& scroll_delta) { + switch (scroll_consumption_state_) { + case DISABLED: + return false; + + case AWAITING_SCROLL_UPDATE_ACK: + // If the initial scroll motion is downward, never allow activation. + if (scroll_delta.y() <= 0) + scroll_consumption_state_ = DISABLED; + return false; + + case ENABLED: + handler_->PullUpdate(scroll_delta.y()); + return true; + } + + NOTREACHED() << "Invalid overscroll state: " << scroll_consumption_state_; + return false; +} + +void OverscrollRefresh::ReleaseWithoutActivation() { + bool allow_activation = false; + Release(allow_activation); +} + +bool OverscrollRefresh::IsActive() const { + return scroll_consumption_state_ == ENABLED; +} + +bool OverscrollRefresh::IsAwaitingScrollUpdateAck() const { + return scroll_consumption_state_ == AWAITING_SCROLL_UPDATE_ACK; +} + +void OverscrollRefresh::OnFrameUpdated( + const gfx::Vector2dF& content_scroll_offset, + bool root_overflow_y_hidden) { + scrolled_to_top_ = content_scroll_offset.y() == 0; + overflow_y_hidden_ = root_overflow_y_hidden; +} + +void OverscrollRefresh::Release(bool allow_refresh) { + if (scroll_consumption_state_ == ENABLED) + handler_->PullRelease(allow_refresh); + scroll_consumption_state_ = DISABLED; +} + +} // namespace content diff --git a/content/browser/android/overscroll_refresh.h b/content/browser/android/overscroll_refresh.h index 77549c95839f..43ed08332580 100644 --- a/content/browser/android/overscroll_refresh.h +++ b/content/browser/android/overscroll_refresh.h @@ -5,34 +5,32 @@ #ifndef CONTENT_BROWSER_ANDROID_OVERSCROLL_REFRESH_H_ #define CONTENT_BROWSER_ANDROID_OVERSCROLL_REFRESH_H_ -#include "base/memory/scoped_ptr.h" -#include "base/time/time.h" +#include "base/macros.h" #include "content/common/content_export.h" #include "ui/gfx/geometry/size_f.h" #include "ui/gfx/geometry/vector2d_f.h" -namespace cc { -class Layer; -} - -namespace ui { -class ResourceManager; -} - namespace content { -// Allows both page reload activation and page reloading state queries. -class CONTENT_EXPORT OverscrollRefreshClient { +class CONTENT_EXPORT OverscrollRefreshHandler { public: - virtual ~OverscrollRefreshClient() {} + // Signals the start of an overscrolling pull. Returns whether the handler + // will consume the overscroll gesture, in which case it will receive the + // remaining pull updates. + virtual bool PullStart() = 0; + + // Signals a pull update, where |delta| is in device pixels. + virtual void PullUpdate(float delta) = 0; - // Called when the effect is released beyond the activation threshold. This - // should cause a refresh of some kind, e.g., page reload. - virtual void TriggerRefresh() = 0; + // Signals the release of the pull, and whether the release is allowed to + // trigger the refresh action. + virtual void PullRelease(bool allow_refresh) = 0; - // Whether the triggered refresh has yet to complete. The effect will continue - // animating until the refresh completes (or it reaches a reasonable timeout). - virtual bool IsStillRefreshing() const = 0; + // Reset the active pull state. + virtual void PullReset() = 0; + + protected: + virtual ~OverscrollRefreshHandler() {} }; // Simple pull-to-refresh styled effect. Listens to scroll events, conditionally @@ -41,8 +39,8 @@ class CONTENT_EXPORT OverscrollRefreshClient { // offset and 2) lacks the overflow-y:hidden property. // 2) The page doesn't consume the initial scroll events. // 3) The initial scroll direction is upward. -// The actual page reload action is triggered only when the effect is active -// and beyond a particular threshold when released. +// The actuall pull response, animation and action are delegated to the +// provided refresh handler. class CONTENT_EXPORT OverscrollRefresh { public: // Minmum number of overscrolling pull events required to activate the effect. @@ -50,18 +48,12 @@ class CONTENT_EXPORT OverscrollRefresh { // capping the impulse per event. enum { kMinPullsToActivate = 3 }; - // Both |resource_manager| and |client| must not be null. - // |target_drag_offset_pixels| is the threshold beyond which the effect - // will trigger a refresh action when released. When |mirror| is true, - // the effect and its rotation will be mirrored about the y axis. - OverscrollRefresh(ui::ResourceManager* resource_manager, - OverscrollRefreshClient* client, - float target_drag_offset_pixels, - bool mirror); + explicit OverscrollRefresh(OverscrollRefreshHandler* handler); ~OverscrollRefresh(); // Scroll event stream listening methods. void OnScrollBegin(); + // Returns whether the refresh was activated. void OnScrollEnd(const gfx::Vector2dF& velocity); // Scroll ack listener. The effect will only be activated if the initial @@ -74,16 +66,11 @@ class CONTENT_EXPORT OverscrollRefresh { // Release the effect (if active), preventing any associated refresh action. void ReleaseWithoutActivation(); - // Returns true if the effect still needs animation ticks, with effect layers - // attached to |parent| if necessary. - // Note: The effect will detach itself when no further animation is required. - bool Animate(base::TimeTicks current_time, cc::Layer* parent_layer); - - // Update the effect according to the most recent display parameters, - // Note: All dimensions are in device pixels. - void UpdateDisplay(const gfx::SizeF& viewport_size, - const gfx::Vector2dF& content_scroll_offset, - bool root_overflow_y_hidden); + // Notify the effect of the latest scroll offset and overflow properties. + // The effect will be disabled when the offset is non-zero or overflow is + // hidden. Note: All dimensions are in device pixels. + void OnFrameUpdated(const gfx::Vector2dF& content_scroll_offset, + bool root_overflow_y_hidden); // Reset the effect to its inactive state, immediately detaching and // disabling any active effects. @@ -96,11 +83,8 @@ class CONTENT_EXPORT OverscrollRefresh { bool IsAwaitingScrollUpdateAck() const; private: - void Release(bool allow_activation); - - OverscrollRefreshClient* const client_; + void Release(bool allow_refresh); - gfx::SizeF viewport_size_; bool scrolled_to_top_; bool overflow_y_hidden_; @@ -110,8 +94,7 @@ class CONTENT_EXPORT OverscrollRefresh { ENABLED, } scroll_consumption_state_; - class Effect; - scoped_ptr effect_; + OverscrollRefreshHandler* const handler_; DISALLOW_COPY_AND_ASSIGN(OverscrollRefresh); }; diff --git a/content/browser/android/overscroll_refresh_unittest.cc b/content/browser/android/overscroll_refresh_unittest.cc dissimilarity index 83% index 45b1dea14b22..70ed0dbd510d 100644 --- a/content/browser/android/overscroll_refresh_unittest.cc +++ b/content/browser/android/overscroll_refresh_unittest.cc @@ -1,314 +1,241 @@ -// Copyright 2014 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "cc/layers/layer.h" -#include "content/browser/android/overscroll_refresh.h" -#include "testing/gtest/include/gtest/gtest.h" -#include "ui/android/resources/resource_manager.h" - -namespace content { - -class OverscrollRefreshTest : public OverscrollRefreshClient, - public ui::ResourceManager, - public testing::Test { - public: - OverscrollRefreshTest() - : refresh_triggered_(false), still_refreshing_(false) {} - - // OverscrollRefreshClient implementation. - void TriggerRefresh() override { - refresh_triggered_ = true; - still_refreshing_ = true; - } - - bool IsStillRefreshing() const override { return still_refreshing_; } - - // ResourceManager implementation. - base::android::ScopedJavaLocalRef GetJavaObject() override { - return base::android::ScopedJavaLocalRef(); - } - - Resource* GetResource(ui::AndroidResourceType res_type, int res_id) override { - return nullptr; - } - - void PreloadResource(ui::AndroidResourceType res_type, int res_id) override {} - - bool GetAndResetRefreshTriggered() { - bool triggered = refresh_triggered_; - refresh_triggered_ = false; - return triggered; - } - - void PullBeyondActivationThreshold(OverscrollRefresh* effect) { - for (int i = 0; i < OverscrollRefresh::kMinPullsToActivate; ++i) - EXPECT_TRUE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 100))); - } - - protected: - - scoped_ptr CreateEffect() { - const float kDragTargetPixels = 100; - const bool kMirror = false; - scoped_ptr effect( - new OverscrollRefresh(this, this, kDragTargetPixels, kMirror)); - - const gfx::SizeF kViewportSize(512, 512); - const gfx::Vector2dF kScrollOffset; - const bool kOverflowYHidden = false; - effect->UpdateDisplay(kViewportSize, kScrollOffset, kOverflowYHidden); - - return effect.Pass(); - } - - void SignalRefreshCompleted() { still_refreshing_ = false; } - - private: - bool refresh_triggered_; - bool still_refreshing_; -}; - -TEST_F(OverscrollRefreshTest, Basic) { - scoped_ptr effect = CreateEffect(); - - EXPECT_FALSE(effect->IsActive()); - EXPECT_FALSE(effect->IsAwaitingScrollUpdateAck()); - - effect->OnScrollBegin(); - EXPECT_FALSE(effect->IsActive()); - EXPECT_TRUE(effect->IsAwaitingScrollUpdateAck()); - - // The initial scroll should not be consumed, as it should first be offered - // to content. - gfx::Vector2dF scroll_up(0, 10); - EXPECT_FALSE(effect->WillHandleScrollUpdate(scroll_up)); - EXPECT_FALSE(effect->IsActive()); - EXPECT_TRUE(effect->IsAwaitingScrollUpdateAck()); - - // The unconsumed, overscrolling scroll will trigger the effect-> - effect->OnScrollUpdateAck(false); - EXPECT_TRUE(effect->IsActive()); - EXPECT_FALSE(effect->IsAwaitingScrollUpdateAck()); - - // Further scrolls will be consumed. - EXPECT_TRUE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 50))); - EXPECT_TRUE(effect->IsActive()); - - // Even scrolls in the down direction should be consumed. - EXPECT_TRUE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, -50))); - EXPECT_TRUE(effect->IsActive()); - - // Feed enough scrolls to the effect to exceeds the threshold. - PullBeyondActivationThreshold(effect.get()); - EXPECT_TRUE(effect->IsActive()); - - // Ending the scroll while beyond the threshold should trigger a refresh. - gfx::Vector2dF zero_velocity; - EXPECT_FALSE(GetAndResetRefreshTriggered()); - effect->OnScrollEnd(zero_velocity); - EXPECT_TRUE(effect->IsActive()); - EXPECT_TRUE(GetAndResetRefreshTriggered()); - SignalRefreshCompleted(); - - // Ensure animation doesn't explode. - base::TimeTicks initial_time = base::TimeTicks::Now(); - base::TimeTicks current_time = initial_time; - scoped_refptr layer = cc::Layer::Create(); - while (effect->Animate(current_time, layer.get())) - current_time += base::TimeDelta::FromMilliseconds(16); - - // The effect should terminate in a timely fashion. - EXPECT_GT(current_time.ToInternalValue(), initial_time.ToInternalValue()); - EXPECT_LE( - current_time.ToInternalValue(), - (initial_time + base::TimeDelta::FromSeconds(10)).ToInternalValue()); - EXPECT_FALSE(effect->IsActive()); -} - -TEST_F(OverscrollRefreshTest, AnimationTerminatesEvenIfRefreshNeverTerminates) { - scoped_ptr effect = CreateEffect(); - effect->OnScrollBegin(); - ASSERT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); - ASSERT_TRUE(effect->IsAwaitingScrollUpdateAck()); - effect->OnScrollUpdateAck(false); - ASSERT_TRUE(effect->IsActive()); - PullBeyondActivationThreshold(effect.get()); - ASSERT_TRUE(effect->IsActive()); - effect->OnScrollEnd(gfx::Vector2dF(0, 0)); - ASSERT_TRUE(GetAndResetRefreshTriggered()); - - // Verify that the animation terminates even if the triggered refresh - // action never terminates (i.e., |still_refreshing_| is always true). - base::TimeTicks initial_time = base::TimeTicks::Now(); - base::TimeTicks current_time = initial_time; - scoped_refptr layer = cc::Layer::Create(); - while (effect->Animate(current_time, layer.get())) - current_time += base::TimeDelta::FromMilliseconds(16); - - EXPECT_GT(current_time.ToInternalValue(), initial_time.ToInternalValue()); - EXPECT_LE( - current_time.ToInternalValue(), - (initial_time + base::TimeDelta::FromSeconds(10)).ToInternalValue()); - EXPECT_FALSE(effect->IsActive()); -} - -TEST_F(OverscrollRefreshTest, NotTriggeredIfBelowThreshold) { - scoped_ptr effect = CreateEffect(); - effect->OnScrollBegin(); - ASSERT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); - ASSERT_TRUE(effect->IsAwaitingScrollUpdateAck()); - effect->OnScrollUpdateAck(false); - ASSERT_TRUE(effect->IsActive()); - - // Terminating the pull before it exceeds the threshold will prevent refresh. - EXPECT_TRUE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); - effect->OnScrollEnd(gfx::Vector2dF()); - EXPECT_FALSE(GetAndResetRefreshTriggered()); -} - -TEST_F(OverscrollRefreshTest, NotTriggeredIfInitialYOffsetIsNotZero) { - scoped_ptr effect = CreateEffect(); - - // A positive y scroll offset at the start of scroll will prevent activation, - // even if the subsequent scroll overscrolls upward. - gfx::SizeF viewport_size(512, 512); - gfx::Vector2dF nonzero_offset(0, 10); - bool overflow_y_hidden = false; - effect->UpdateDisplay(viewport_size, nonzero_offset, overflow_y_hidden); - effect->OnScrollBegin(); - - ASSERT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); - EXPECT_FALSE(effect->IsActive()); - EXPECT_FALSE(effect->IsAwaitingScrollUpdateAck()); - effect->OnScrollUpdateAck(false); - EXPECT_FALSE(effect->IsActive()); - EXPECT_FALSE(effect->IsAwaitingScrollUpdateAck()); - EXPECT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 500))); - effect->OnScrollEnd(gfx::Vector2dF()); - EXPECT_FALSE(GetAndResetRefreshTriggered()); -} - -TEST_F(OverscrollRefreshTest, NotTriggeredIfOverflowYHidden) { - scoped_ptr effect = CreateEffect(); - - // "overflow-y: hidden" on the root layer will prevent activation, - // even if the subsequent scroll overscrolls upward. - gfx::SizeF viewport_size(512, 512); - gfx::Vector2dF zero_offset; - bool overflow_y_hidden = true; - effect->UpdateDisplay(viewport_size, zero_offset, overflow_y_hidden); - effect->OnScrollBegin(); - ASSERT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); - EXPECT_FALSE(effect->IsActive()); - EXPECT_FALSE(effect->IsAwaitingScrollUpdateAck()); - effect->OnScrollUpdateAck(false); - EXPECT_FALSE(effect->IsActive()); - EXPECT_FALSE(effect->IsAwaitingScrollUpdateAck()); - EXPECT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 500))); - effect->OnScrollEnd(gfx::Vector2dF()); - EXPECT_FALSE(GetAndResetRefreshTriggered()); -} - -TEST_F(OverscrollRefreshTest, NotTriggeredIfInitialScrollDownward) { - scoped_ptr effect = CreateEffect(); - effect->OnScrollBegin(); - - // A downward initial scroll will prevent activation, even if the subsequent - // scroll overscrolls upward. - ASSERT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, -10))); - EXPECT_FALSE(effect->IsActive()); - EXPECT_FALSE(effect->IsAwaitingScrollUpdateAck()); - - effect->OnScrollUpdateAck(false); - EXPECT_FALSE(effect->IsActive()); - EXPECT_FALSE(effect->IsAwaitingScrollUpdateAck()); - EXPECT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 500))); - effect->OnScrollEnd(gfx::Vector2dF()); - EXPECT_FALSE(GetAndResetRefreshTriggered()); -} - -TEST_F(OverscrollRefreshTest, NotTriggeredIfInitialScrollOrTouchConsumed) { - scoped_ptr effect = CreateEffect(); - effect->OnScrollBegin(); - ASSERT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); - ASSERT_TRUE(effect->IsAwaitingScrollUpdateAck()); - - // Consumption of the initial touchmove or scroll should prevent future - // activation. - effect->OnScrollUpdateAck(true); - EXPECT_FALSE(effect->IsActive()); - EXPECT_FALSE(effect->IsAwaitingScrollUpdateAck()); - EXPECT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 500))); - effect->OnScrollUpdateAck(false); - EXPECT_FALSE(effect->IsActive()); - EXPECT_FALSE(effect->IsAwaitingScrollUpdateAck()); - EXPECT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 500))); - effect->OnScrollEnd(gfx::Vector2dF()); - EXPECT_FALSE(GetAndResetRefreshTriggered()); -} - -TEST_F(OverscrollRefreshTest, NotTriggeredIfInitialScrollsJanked) { - scoped_ptr effect = CreateEffect(); - effect->OnScrollBegin(); - ASSERT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); - ASSERT_TRUE(effect->IsAwaitingScrollUpdateAck()); - effect->OnScrollUpdateAck(false); - ASSERT_TRUE(effect->IsActive()); - - // It should take more than just one or two large scrolls to trigger, - // mitigating likelihood of jank triggering the effect-> - EXPECT_TRUE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 500))); - EXPECT_TRUE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 500))); - effect->OnScrollEnd(gfx::Vector2dF()); - EXPECT_FALSE(GetAndResetRefreshTriggered()); -} - -TEST_F(OverscrollRefreshTest, NotTriggeredIfFlungDownward) { - scoped_ptr effect = CreateEffect(); - effect->OnScrollBegin(); - ASSERT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); - ASSERT_TRUE(effect->IsAwaitingScrollUpdateAck()); - effect->OnScrollUpdateAck(false); - ASSERT_TRUE(effect->IsActive()); - - // Ensure the pull exceeds the necessary threshold. - PullBeyondActivationThreshold(effect.get()); - ASSERT_TRUE(effect->IsActive()); - - // Terminating the pull with a down-directed fling should prevent triggering. - effect->OnScrollEnd(gfx::Vector2dF(0, -1000)); - EXPECT_FALSE(GetAndResetRefreshTriggered()); -} - -TEST_F(OverscrollRefreshTest, NotTriggeredIfReleasedWithoutActivation) { - scoped_ptr effect = CreateEffect(); - effect->OnScrollBegin(); - ASSERT_FALSE(effect->WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); - ASSERT_TRUE(effect->IsAwaitingScrollUpdateAck()); - effect->OnScrollUpdateAck(false); - ASSERT_TRUE(effect->IsActive()); - - // Ensure the pull exceeds the necessary threshold. - PullBeyondActivationThreshold(effect.get()); - ASSERT_TRUE(effect->IsActive()); - - // An early release should prevent the refresh action from firing. - effect->ReleaseWithoutActivation(); - effect->OnScrollEnd(gfx::Vector2dF()); - EXPECT_FALSE(GetAndResetRefreshTriggered()); - - // The early release should trigger a dismissal animation. - EXPECT_TRUE(effect->IsActive()); - base::TimeTicks initial_time = base::TimeTicks::Now(); - base::TimeTicks current_time = initial_time; - scoped_refptr layer = cc::Layer::Create(); - while (effect->Animate(current_time, layer.get())) - current_time += base::TimeDelta::FromMilliseconds(16); - - EXPECT_GT(current_time.ToInternalValue(), initial_time.ToInternalValue()); - EXPECT_FALSE(effect->IsActive()); - EXPECT_FALSE(GetAndResetRefreshTriggered()); -} - -} // namespace content +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cc/layers/layer.h" +#include "content/browser/android/overscroll_refresh.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +class OverscrollRefreshTest : public OverscrollRefreshHandler, + public testing::Test { + public: + OverscrollRefreshTest() {} + + // OverscrollRefreshHandler implementation. + bool PullStart() override { + started_ = true; + return true; + } + + void PullUpdate(float delta) override { delta_ += delta; } + + void PullRelease(bool allow_refresh) override { + released_ = true; + refresh_allowed_ = allow_refresh; + } + + void PullReset() override { reset_ = true; } + + bool GetAndResetPullStarted() { + bool result = started_; + started_ = false; + return result; + } + + float GetAndResetPullDelta() { + float result = delta_; + delta_ = 0; + return result; + } + + bool GetAndResetPullReleased() { + bool result = released_; + released_ = false; + return result; + } + + bool GetAndResetRefreshAllowed() { + bool result = refresh_allowed_; + refresh_allowed_ = false; + return result; + } + + bool GetAndResetPullReset() { + bool result = reset_; + reset_ = false; + return result; + } + + private: + float delta_ = 0; + bool started_ = false; + bool released_ = false; + bool reset_ = false; + bool refresh_allowed_ = false; +}; + +TEST_F(OverscrollRefreshTest, Basic) { + OverscrollRefresh effect(this); + + EXPECT_FALSE(effect.IsActive()); + EXPECT_FALSE(effect.IsAwaitingScrollUpdateAck()); + + effect.OnScrollBegin(); + EXPECT_FALSE(effect.IsActive()); + EXPECT_TRUE(effect.IsAwaitingScrollUpdateAck()); + + // The initial scroll should not be consumed, as it should first be offered + // to content. + gfx::Vector2dF scroll_up(0, 10); + EXPECT_FALSE(effect.WillHandleScrollUpdate(scroll_up)); + EXPECT_FALSE(effect.IsActive()); + EXPECT_TRUE(effect.IsAwaitingScrollUpdateAck()); + + // The unconsumed, overscrolling scroll will trigger the effect. + effect.OnScrollUpdateAck(false); + EXPECT_TRUE(effect.IsActive()); + EXPECT_FALSE(effect.IsAwaitingScrollUpdateAck()); + EXPECT_TRUE(GetAndResetPullStarted()); + + // Further scrolls will be consumed. + EXPECT_TRUE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, 50))); + EXPECT_EQ(50.f, GetAndResetPullDelta()); + EXPECT_TRUE(effect.IsActive()); + + // Even scrolls in the down direction should be consumed. + EXPECT_TRUE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, -50))); + EXPECT_EQ(-50.f, GetAndResetPullDelta()); + EXPECT_TRUE(effect.IsActive()); + + // Ending the scroll while beyond the threshold should trigger a refresh. + gfx::Vector2dF zero_velocity; + EXPECT_FALSE(GetAndResetPullReleased()); + effect.OnScrollEnd(zero_velocity); + EXPECT_FALSE(effect.IsActive()); + EXPECT_TRUE(GetAndResetPullReleased()); + EXPECT_TRUE(GetAndResetRefreshAllowed()); +} + +TEST_F(OverscrollRefreshTest, NotTriggeredIfInitialYOffsetIsNotZero) { + OverscrollRefresh effect(this); + + // A positive y scroll offset at the start of scroll will prevent activation, + // even if the subsequent scroll overscrolls upward. + gfx::Vector2dF nonzero_offset(0, 10); + bool overflow_y_hidden = false; + effect.OnFrameUpdated(nonzero_offset, overflow_y_hidden); + effect.OnScrollBegin(); + + effect.OnFrameUpdated(gfx::Vector2dF(), overflow_y_hidden); + ASSERT_FALSE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); + EXPECT_FALSE(effect.IsActive()); + EXPECT_FALSE(effect.IsAwaitingScrollUpdateAck()); + effect.OnScrollUpdateAck(false); + EXPECT_FALSE(effect.IsActive()); + EXPECT_FALSE(effect.IsAwaitingScrollUpdateAck()); + EXPECT_FALSE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, 500))); + effect.OnScrollEnd(gfx::Vector2dF()); + EXPECT_FALSE(GetAndResetPullStarted()); + EXPECT_FALSE(GetAndResetPullReleased()); +} + +TEST_F(OverscrollRefreshTest, NotTriggeredIfOverflowYHidden) { + OverscrollRefresh effect(this); + + // overflow-y:hidden at the start of scroll will prevent activation. + gfx::Vector2dF zero_offset; + bool overflow_y_hidden = true; + effect.OnFrameUpdated(zero_offset, overflow_y_hidden); + effect.OnScrollBegin(); + + ASSERT_FALSE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); + EXPECT_FALSE(effect.IsActive()); + EXPECT_FALSE(effect.IsAwaitingScrollUpdateAck()); + effect.OnScrollUpdateAck(false); + EXPECT_FALSE(effect.IsActive()); + EXPECT_FALSE(effect.IsAwaitingScrollUpdateAck()); + EXPECT_FALSE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, 500))); + effect.OnScrollEnd(gfx::Vector2dF()); + EXPECT_FALSE(GetAndResetPullStarted()); + EXPECT_FALSE(GetAndResetPullReleased()); +} + +TEST_F(OverscrollRefreshTest, NotTriggeredIfInitialScrollDownward) { + OverscrollRefresh effect(this); + effect.OnScrollBegin(); + + // A downward initial scroll will prevent activation, even if the subsequent + // scroll overscrolls upward. + ASSERT_FALSE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, -10))); + EXPECT_FALSE(effect.IsActive()); + EXPECT_FALSE(effect.IsAwaitingScrollUpdateAck()); + + effect.OnScrollUpdateAck(false); + EXPECT_FALSE(effect.IsActive()); + EXPECT_FALSE(effect.IsAwaitingScrollUpdateAck()); + EXPECT_FALSE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, 500))); + effect.OnScrollEnd(gfx::Vector2dF()); + EXPECT_FALSE(GetAndResetPullReleased()); +} + +TEST_F(OverscrollRefreshTest, NotTriggeredIfInitialScrollOrTouchConsumed) { + OverscrollRefresh effect(this); + effect.OnScrollBegin(); + ASSERT_FALSE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); + ASSERT_TRUE(effect.IsAwaitingScrollUpdateAck()); + + // Consumption of the initial touchmove or scroll should prevent future + // activation. + effect.OnScrollUpdateAck(true); + EXPECT_FALSE(effect.IsActive()); + EXPECT_FALSE(effect.IsAwaitingScrollUpdateAck()); + EXPECT_FALSE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, 500))); + effect.OnScrollUpdateAck(false); + EXPECT_FALSE(effect.IsActive()); + EXPECT_FALSE(effect.IsAwaitingScrollUpdateAck()); + EXPECT_FALSE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, 500))); + effect.OnScrollEnd(gfx::Vector2dF()); + EXPECT_FALSE(GetAndResetPullStarted()); + EXPECT_FALSE(GetAndResetPullReleased()); +} + +TEST_F(OverscrollRefreshTest, NotTriggeredIfFlungDownward) { + OverscrollRefresh effect(this); + effect.OnScrollBegin(); + ASSERT_FALSE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); + ASSERT_TRUE(effect.IsAwaitingScrollUpdateAck()); + effect.OnScrollUpdateAck(false); + ASSERT_TRUE(effect.IsActive()); + EXPECT_TRUE(GetAndResetPullStarted()); + + // Terminating the pull with a down-directed fling should prevent triggering. + effect.OnScrollEnd(gfx::Vector2dF(0, -1000)); + EXPECT_TRUE(GetAndResetPullReleased()); + EXPECT_FALSE(GetAndResetRefreshAllowed()); +} + +TEST_F(OverscrollRefreshTest, NotTriggeredIfReleasedWithoutActivation) { + OverscrollRefresh effect(this); + effect.OnScrollBegin(); + ASSERT_FALSE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); + ASSERT_TRUE(effect.IsAwaitingScrollUpdateAck()); + effect.OnScrollUpdateAck(false); + ASSERT_TRUE(effect.IsActive()); + EXPECT_TRUE(GetAndResetPullStarted()); + + // An early release should prevent the refresh action from firing. + effect.ReleaseWithoutActivation(); + effect.OnScrollEnd(gfx::Vector2dF()); + EXPECT_TRUE(GetAndResetPullReleased()); + EXPECT_FALSE(GetAndResetRefreshAllowed()); +} + +TEST_F(OverscrollRefreshTest, NotTriggeredIfReset) { + OverscrollRefresh effect(this); + effect.OnScrollBegin(); + ASSERT_FALSE(effect.WillHandleScrollUpdate(gfx::Vector2dF(0, 10))); + ASSERT_TRUE(effect.IsAwaitingScrollUpdateAck()); + effect.OnScrollUpdateAck(false); + ASSERT_TRUE(effect.IsActive()); + EXPECT_TRUE(GetAndResetPullStarted()); + + // An early reset should prevent the refresh action from firing. + effect.Reset(); + EXPECT_TRUE(GetAndResetPullReset()); + effect.OnScrollEnd(gfx::Vector2dF()); + EXPECT_FALSE(GetAndResetPullReleased()); +} + +} // namespace content diff --git a/content/browser/renderer_host/render_widget_host_view_android.cc b/content/browser/renderer_host/render_widget_host_view_android.cc index 7815e8b31f92..d7248af0bc99 100644 --- a/content/browser/renderer_host/render_widget_host_view_android.cc +++ b/content/browser/renderer_host/render_widget_host_view_android.cc @@ -293,16 +293,8 @@ scoped_ptr CreateSelectionController( } scoped_ptr CreateOverscrollController( - ContentViewCore* content_view_core) { - DCHECK(content_view_core); - ui::WindowAndroid* window = content_view_core->GetWindowAndroid(); - DCHECK(window); - ui::WindowAndroidCompositor* compositor = window->GetCompositor(); - DCHECK(compositor); - return make_scoped_ptr(new OverscrollControllerAndroid( - content_view_core->GetWebContents(), - compositor, - content_view_core->GetDpiScale())); + ContentViewCoreImpl* content_view_core) { + return make_scoped_ptr(new OverscrollControllerAndroid(content_view_core)); } ui::GestureProvider::Config CreateGestureProviderConfig() { diff --git a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java index 9af5dfd2eb52..f18b102aee9b 100644 --- a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java +++ b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java @@ -494,6 +494,8 @@ public class ContentViewCore private SelectPopup mSelectPopup; private long mNativeSelectPopupSourceFrame = 0; + private OverscrollRefreshHandler mOverscrollRefreshHandler; + private Runnable mFakeMouseMoveRunnable = null; // Only valid when focused on a text / password field. @@ -871,6 +873,7 @@ public class ContentViewCore try { TraceEvent.begin("ContentViewCore.setContainerView"); if (mContainerView != null) { + assert mOverscrollRefreshHandler == null; mPastePopupMenu = null; mInputConnection = null; hidePopupsAndClearSelection(); @@ -1002,6 +1005,7 @@ public class ContentViewCore // in this class. mContentViewClient = new ContentViewClient(); mWebContents = null; + mOverscrollRefreshHandler = null; mNativeContentViewCore = 0; mJavaScriptInterfaces.clear(); mRetainedJavaScriptObjects.clear(); @@ -2513,6 +2517,40 @@ public class ContentViewCore return new PopupTouchHandleDrawable(mTouchHandleDelegate); } + /** + * Initialize the view with an overscroll refresh handler. + * @param handler The refresh handler. + */ + public void setOverscrollRefreshHandler(OverscrollRefreshHandler handler) { + assert mOverscrollRefreshHandler == null || handler == null; + mOverscrollRefreshHandler = handler; + } + + @SuppressWarnings("unused") + @CalledByNative + private boolean onOverscrollRefreshStart() { + if (mOverscrollRefreshHandler == null) return false; + return mOverscrollRefreshHandler.start(); + } + + @SuppressWarnings("unused") + @CalledByNative + private void onOverscrollRefreshUpdate(float delta) { + if (mOverscrollRefreshHandler != null) mOverscrollRefreshHandler.pull(delta); + } + + @SuppressWarnings("unused") + @CalledByNative + private void onOverscrollRefreshRelease(boolean allowRefresh) { + if (mOverscrollRefreshHandler != null) mOverscrollRefreshHandler.release(allowRefresh); + } + + @SuppressWarnings("unused") + @CalledByNative + private void onOverscrollRefreshReset() { + if (mOverscrollRefreshHandler != null) mOverscrollRefreshHandler.reset(); + } + @SuppressWarnings("unused") @CalledByNative private void onSelectionChanged(String text) { diff --git a/content/public/android/java/src/org/chromium/content/browser/OverscrollRefreshHandler.java b/content/public/android/java/src/org/chromium/content/browser/OverscrollRefreshHandler.java new file mode 100644 index 000000000000..8717b8384fe5 --- /dev/null +++ b/content/public/android/java/src/org/chromium/content/browser/OverscrollRefreshHandler.java @@ -0,0 +1,40 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +/** + * Simple interface allowing customized response to an overscrolling pull input. + */ +public interface OverscrollRefreshHandler { + /** + * Signals the start of an overscrolling pull. + * @return Whether the handler will consume the overscroll sequence. + */ + public boolean start(); + + /** + * Signals a pull update. + * @param delta The change in pull distance (positive if pulling down, negative if up). + */ + public void pull(float delta); + + /** + * Signals the release of the pull. + * @param allowRefresh Whether the release signal should be allowed to trigger a refresh. + */ + public void release(boolean allowRefresh); + + /** + * Reset the active pull state. + */ + public void reset(); + + /** + * Toggle whether the effect is active. + * @param enabled Whether to enable the effect. + * If disabled, the effect should deactive itself apropriately. + */ + public void setEnabled(boolean enabled); +} diff --git a/third_party/android_swipe_refresh/BUILD.gn b/third_party/android_swipe_refresh/BUILD.gn index 079f6faf3729..ad14d8197f77 100644 --- a/third_party/android_swipe_refresh/BUILD.gn +++ b/third_party/android_swipe_refresh/BUILD.gn @@ -8,7 +8,4 @@ assert(is_android) android_library("android_swipe_refresh_java") { DEPRECATED_java_in_dir = "java/src" - deps = [ - "//third_party/android_tools:android_support_v4_java", - ] } diff --git a/third_party/android_swipe_refresh/README.chromium b/third_party/android_swipe_refresh/README.chromium index 686e17767def..9cdec7c22009 100644 --- a/third_party/android_swipe_refresh/README.chromium +++ b/third_party/android_swipe_refresh/README.chromium @@ -10,5 +10,12 @@ dependencies) from Android's app compat library (v4). The widget provides a pull-to-refresh styled layout for touch-activated refresh of view contents. Local Modifications: -CircleImageView, MaterialProgressDrawable, SwipeRefreshLayout: - * Only the package has been changed. + +CircleImageView, MaterialProgressDrawable + * The package has been changed, and all ViewCompat dependencies removed. + +SwipeRefreshLayout + * MotionEvent-behavior has been changed to allow more abstract inputs. + * Target View semantics have been removed, allowing it to operate on + a parent View rather than a child View. + * All ViewCompat and MotionEventCompat dependencies removed. diff --git a/third_party/android_swipe_refresh/android_swipe_refresh.gyp b/third_party/android_swipe_refresh/android_swipe_refresh.gyp index 8f9543945695..52e409b1af58 100644 --- a/third_party/android_swipe_refresh/android_swipe_refresh.gyp +++ b/third_party/android_swipe_refresh/android_swipe_refresh.gyp @@ -7,9 +7,6 @@ { 'target_name': 'android_swipe_refresh_java', 'type': 'none', - 'dependencies': [ - '../../third_party/android_tools/android_tools.gyp:android_support_v4_javalib', - ], 'variables': { 'java_in_dir': 'java', }, diff --git a/third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/CircleImageView.java b/third_party/android_swipe_refresh/java/src/org/chromium/third_party/android/swiperefresh/CircleImageView.java similarity index 79% rename from third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/CircleImageView.java rename to third_party/android_swipe_refresh/java/src/org/chromium/third_party/android/swiperefresh/CircleImageView.java index 94963925475d..fcb2185aeb88 100644 --- a/third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/CircleImageView.java +++ b/third_party/android_swipe_refresh/java/src/org/chromium/third_party/android/swiperefresh/CircleImageView.java @@ -25,8 +25,8 @@ import android.graphics.RadialGradient; import android.graphics.Shader; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; -import android.support.v4.view.ViewCompat; import android.view.animation.Animation; +import android.view.View; import android.widget.ImageView; /** @@ -49,28 +49,18 @@ class CircleImageView extends ImageView { private Animation.AnimationListener mListener; private int mShadowRadius; + @SuppressWarnings("deprecation") public CircleImageView(Context context, int color, final float radius) { super(context); final float density = getContext().getResources().getDisplayMetrics().density; - final int diameter = (int) (radius * density * 2); - final int shadowYOffset = (int) (density * Y_OFFSET); - final int shadowXOffset = (int) (density * X_OFFSET); mShadowRadius = (int) (density * SHADOW_RADIUS); ShapeDrawable circle; if (elevationSupported()) { - circle = new ShapeDrawable(new OvalShape()); - ViewCompat.setElevation(this, SHADOW_ELEVATION * density); + circle = initializeElevated(density); } else { - OvalShape oval = new OvalShadow(mShadowRadius, diameter); - circle = new ShapeDrawable(oval); - ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, circle.getPaint()); - circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, - KEY_SHADOW_COLOR); - final int padding = (int) mShadowRadius; - // set padding so the inner image sits correctly within the shadow. - setPadding(padding, padding, padding, padding); + circle = initializeNonElevated(radius, density); } circle.getPaint().setColor(color); if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) { @@ -80,6 +70,27 @@ class CircleImageView extends ImageView { } } + ShapeDrawable initializeElevated(float density) { + ShapeDrawable circle = new ShapeDrawable(new OvalShape()); + setElevation(SHADOW_ELEVATION * density); + return circle; + } + + ShapeDrawable initializeNonElevated(float radius, float density) { + final int diameter = (int) (radius * density * 2); + final int shadowYOffset = (int) (density * Y_OFFSET); + final int shadowXOffset = (int) (density * X_OFFSET); + OvalShape oval = new OvalShadow(mShadowRadius, diameter); + ShapeDrawable circle = new ShapeDrawable(oval); + setLayerType(View.LAYER_TYPE_SOFTWARE, circle.getPaint()); + circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, + KEY_SHADOW_COLOR); + final int padding = (int) mShadowRadius; + // set padding so the inner image sits correctly within the shadow. + setPadding(padding, padding, padding, padding); + return circle; + } + private boolean elevationSupported() { return android.os.Build.VERSION.SDK_INT >= 21; } @@ -134,7 +145,7 @@ class CircleImageView extends ImageView { mShadowPaint = new Paint(); mShadowRadius = shadowRadius; mCircleDiameter = circleDiameter; - mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2, + mRadialGradient = new RadialGradient(mCircleDiameter / 2f, mCircleDiameter / 2f, mShadowRadius, new int[] { FILL_SHADOW_COLOR, Color.TRANSPARENT }, null, Shader.TileMode.CLAMP); @@ -145,9 +156,9 @@ class CircleImageView extends ImageView { public void draw(Canvas canvas, Paint paint) { final int viewWidth = CircleImageView.this.getWidth(); final int viewHeight = CircleImageView.this.getHeight(); - canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2 + mShadowRadius), + canvas.drawCircle(viewWidth / 2f, viewHeight / 2f, (mCircleDiameter / 2f + mShadowRadius), mShadowPaint); - canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint); + canvas.drawCircle(viewWidth / 2f, viewHeight / 2f, (mCircleDiameter / 2f), paint); } } } diff --git a/third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/MaterialProgressDrawable.java b/third_party/android_swipe_refresh/java/src/org/chromium/third_party/android/swiperefresh/MaterialProgressDrawable.java similarity index 96% rename from third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/MaterialProgressDrawable.java rename to third_party/android_swipe_refresh/java/src/org/chromium/third_party/android/swiperefresh/MaterialProgressDrawable.java index 2b29444f7c9f..23a6a37dcbb0 100644 --- a/third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/MaterialProgressDrawable.java +++ b/third_party/android_swipe_refresh/java/src/org/chromium/third_party/android/swiperefresh/MaterialProgressDrawable.java @@ -35,8 +35,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.Animatable; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; import android.util.DisplayMetrics; import android.view.View; @@ -56,7 +54,6 @@ class MaterialProgressDrawable extends Drawable implements Animatable { private static final Interpolator EASE_INTERPOLATOR = new AccelerateDecelerateInterpolator(); @Retention(RetentionPolicy.CLASS) - @IntDef({LARGE, DEFAULT}) public @interface ProgressDrawableSize {} // Maps to ProgressBar.Large style static final int LARGE = 0; @@ -215,13 +212,25 @@ class MaterialProgressDrawable extends Drawable implements Animatable { return (int) mWidth; } + private static boolean isLayoutRtl(View view) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } else { + // All layouts are LTR before JB MR1. + return false; + } + } + @Override public void draw(Canvas c) { + final boolean mirrorX = isLayoutRtl(mParent); final Rect bounds = getBounds(); final int saveCount = c.save(); + if (mirrorX) c.scale(-1.f, 1.f); c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); mRing.draw(c, bounds); c.restoreToCount(saveCount); + if (mirrorX) c.scale(-1.f, 1.f); } @Override @@ -358,7 +367,7 @@ class MaterialProgressDrawable extends Drawable implements Animatable { setRotation(groupRotation); } }; - animation.setRepeatCount(Animation.INFINITE); + animation.setRepeatCount(10); animation.setRepeatMode(Animation.RESTART); animation.setInterpolator(LINEAR_INTERPOLATOR); animation.setDuration(ANIMATION_DURATION); @@ -480,7 +489,7 @@ class MaterialProgressDrawable extends Drawable implements Animatable { if (mAlpha < 255) { mCirclePaint.setColor(mBackgroundColor); mCirclePaint.setAlpha(255 - mAlpha); - c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, + c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2f, mCirclePaint); } } @@ -496,7 +505,7 @@ class MaterialProgressDrawable extends Drawable implements Animatable { // Adjust the position of the triangle so that it is inset as // much as the arc, but also centered on the arc. - float inset = (int) mStrokeInset / 2 * mArrowScale; + float inset = (int) mStrokeInset / 2f * mArrowScale; float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); @@ -506,8 +515,7 @@ class MaterialProgressDrawable extends Drawable implements Animatable { // been fixed as of API 21. mArrow.moveTo(0, 0); mArrow.lineTo(mArrowWidth * mArrowScale, 0); - mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight - * mArrowScale)); + mArrow.lineTo((mArrowWidth * mArrowScale / 2f), (mArrowHeight * mArrowScale)); mArrow.offset(x - inset, y); mArrow.close(); // draw a triangle @@ -523,7 +531,7 @@ class MaterialProgressDrawable extends Drawable implements Animatable { * * @param colors Array of integers describing the colors. Must be non-null. */ - public void setColors(@NonNull int[] colors) { + public void setColors(int[] colors) { mColors = colors; // if colors are reset, make sure to reset the color index as well setColorIndex(0); @@ -554,7 +562,10 @@ class MaterialProgressDrawable extends Drawable implements Animatable { * @param alpha Set the alpha of the progress spinner and associated arrowhead. */ public void setAlpha(int alpha) { - mAlpha = alpha; + if (mAlpha != alpha) { + mAlpha = alpha; + invalidateSelf(); + } } /** diff --git a/third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/SwipeRefreshLayout.java b/third_party/android_swipe_refresh/java/src/org/chromium/third_party/android/swiperefresh/SwipeRefreshLayout.java similarity index 61% rename from third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/SwipeRefreshLayout.java rename to third_party/android_swipe_refresh/java/src/org/chromium/third_party/android/swiperefresh/SwipeRefreshLayout.java index dfd4cc2ec7e6..eed8d7763eec 100644 --- a/third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/SwipeRefreshLayout.java +++ b/third_party/android_swipe_refresh/java/src/org/chromium/third_party/android/swiperefresh/SwipeRefreshLayout.java @@ -19,8 +19,6 @@ package org.chromium.third_party.android.swiperefresh; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; -import android.support.v4.view.MotionEventCompat; -import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; @@ -70,7 +68,6 @@ public class SwipeRefreshLayout extends ViewGroup { private static final int CIRCLE_DIAMETER_LARGE = 56; private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; - private static final int INVALID_POINTER = -1; private static final float DRAG_RATE = .5f; // Max amount of circle that can be filled by progress during swipe gesture, @@ -90,10 +87,8 @@ public class SwipeRefreshLayout extends ViewGroup { // Default offset in dips from the top of the view to where the progress spinner should stop private static final int DEFAULT_CIRCLE_TARGET = 64; - private View mTarget; // the target of the gesture private OnRefreshListener mListener; private boolean mRefreshing = false; - private int mTouchSlop; private float mTotalDragDistance = -1; private int mMediumAnimationDuration; private int mCurrentTargetOffsetTop; @@ -102,7 +97,6 @@ public class SwipeRefreshLayout extends ViewGroup { private float mInitialMotionY; private boolean mIsBeingDragged; - private int mActivePointerId = INVALID_POINTER; // Whether this item is scaled up rather than clipped private boolean mScale; @@ -135,6 +129,8 @@ public class SwipeRefreshLayout extends ViewGroup { private Animation mScaleDownToStartAnimation; + private Animation.AnimationListener mCancelAnimationListener; + private float mSpinnerFinalOffset; private boolean mNotify; @@ -167,21 +163,19 @@ public class SwipeRefreshLayout extends ViewGroup { } } } else { - mProgress.stop(); - mCircleView.setVisibility(View.GONE); - setColorViewAlpha(MAX_ALPHA); - // Return the circle to its start position - if (mScale) { - setAnimationProgress(0 /* animation complete and view is hidden */); - } else { - setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop, - true /* requires update */); - } + reset(); } mCurrentTargetOffsetTop = mCircleView.getTop(); } }; + // Chrome-specific additions. + private float mTotalMotionY; + // Minimum number of pull updates necessary to trigger a refresh. + private static int MIN_PULLS_TO_ACTIVATE = 3; + // Multiplier for the default top offset relative to the size of the progress spinner. + private static final float DEFAULT_OFFSET_TOP_MULTIPLIER = 1.05f; + private void setColorViewAlpha(int targetAlpha) { mCircleView.getBackground().setAlpha(targetAlpha); mProgress.setAlpha(targetAlpha); @@ -267,8 +261,6 @@ public class SwipeRefreshLayout extends ViewGroup { public SwipeRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); - mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); - mMediumAnimationDuration = getResources().getInteger( android.R.integer.config_mediumAnimTime); @@ -284,7 +276,7 @@ public class SwipeRefreshLayout extends ViewGroup { mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density); createProgressView(); - ViewCompat.setChildrenDrawingOrderEnabled(this, true); + setChildrenDrawingOrderEnabled(true); // the absolute offset has to take into account that the circle starts at an offset mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density; mTotalDragDistance = mSpinnerFinalOffset; @@ -362,13 +354,15 @@ public class SwipeRefreshLayout extends ViewGroup { // Don't adjust the alpha during appearance otherwise. mProgress.setAlpha(MAX_ALPHA); } - mScaleAnimation = new Animation() { - @Override - public void applyTransformation(float interpolatedTime, Transformation t) { - setAnimationProgress(interpolatedTime); - } - }; - mScaleAnimation.setDuration(mMediumAnimationDuration); + if (mScaleAnimation == null) { + mScaleAnimation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + setAnimationProgress(interpolatedTime); + } + }; + mScaleAnimation.setDuration(mMediumAnimationDuration); + } if (listener != null) { mCircleView.setAnimationListener(listener); } @@ -384,15 +378,14 @@ public class SwipeRefreshLayout extends ViewGroup { if (isAlphaUsedForScale()) { setColorViewAlpha((int) (progress * MAX_ALPHA)); } else { - ViewCompat.setScaleX(mCircleView, progress); - ViewCompat.setScaleY(mCircleView, progress); + mCircleView.setScaleX(progress); + mCircleView.setScaleY(progress); } } private void setRefreshing(boolean refreshing, final boolean notify) { if (mRefreshing != refreshing) { mNotify = notify; - ensureTarget(); mRefreshing = refreshing; if (mRefreshing) { animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); @@ -403,13 +396,15 @@ public class SwipeRefreshLayout extends ViewGroup { } private void startScaleDownAnimation(Animation.AnimationListener listener) { - mScaleDownAnimation = new Animation() { - @Override - public void applyTransformation(float interpolatedTime, Transformation t) { - setAnimationProgress(1 - interpolatedTime); - } - }; - mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); + if (mScaleDownAnimation == null) { + mScaleDownAnimation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + setAnimationProgress(1 - interpolatedTime); + } + }; + mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); + } mCircleView.setAnimationListener(listener); mCircleView.clearAnimation(); mCircleView.startAnimation(mScaleDownAnimation); @@ -487,7 +482,6 @@ public class SwipeRefreshLayout extends ViewGroup { * @param colors */ public void setColorSchemeColors(int... colors) { - ensureTarget(); mProgress.setColorSchemeColors(colors); } @@ -499,20 +493,6 @@ public class SwipeRefreshLayout extends ViewGroup { return mRefreshing; } - private void ensureTarget() { - // Don't bother getting the parent height if the parent hasn't been laid - // out yet. - if (mTarget == null) { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - if (!child.equals(mCircleView)) { - mTarget = child; - break; - } - } - } - } - /** * Set the distance to trigger a sync in dips * @@ -525,22 +505,9 @@ public class SwipeRefreshLayout extends ViewGroup { @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int width = getMeasuredWidth(); - final int height = getMeasuredHeight(); if (getChildCount() == 0) { return; } - if (mTarget == null) { - ensureTarget(); - } - if (mTarget == null) { - return; - } - final View child = mTarget; - final int childLeft = getPaddingLeft(); - final int childTop = getPaddingTop(); - final int childWidth = width - getPaddingLeft() - getPaddingRight(); - final int childHeight = height - getPaddingTop() - getPaddingBottom(); - child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); int circleWidth = mCircleView.getMeasuredWidth(); int circleHeight = mCircleView.getMeasuredHeight(); mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, @@ -550,21 +517,12 @@ public class SwipeRefreshLayout extends ViewGroup { @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (mTarget == null) { - ensureTarget(); - } - if (mTarget == null) { - return; - } - mTarget.measure(MeasureSpec.makeMeasureSpec( - getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), - MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( - getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); if (!mUsingCustomStart && !mOriginalOffsetCalculated) { mOriginalOffsetCalculated = true; - mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); + mCurrentTargetOffsetTop = mOriginalOffsetTop = + (int) (-mCircleView.getMeasuredHeight() * DEFAULT_OFFSET_TOP_MULTIPLIER); } mCircleViewIndex = -1; // Get the index of the circleview. @@ -577,237 +535,146 @@ public class SwipeRefreshLayout extends ViewGroup { } /** - * @return Whether it is possible for the child view of this layout to - * scroll up. Override this if the child view is a custom view. + * Start the pull effect. If the effect is disabled or a refresh animation + * is currently active, the request will be ignored. + * @return whether a new pull sequence has started. */ - public boolean canChildScrollUp() { - if (android.os.Build.VERSION.SDK_INT < 14) { - if (mTarget instanceof AbsListView) { - final AbsListView absListView = (AbsListView) mTarget; - return absListView.getChildCount() > 0 - && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) - .getTop() < absListView.getPaddingTop()); - } else { - return mTarget.getScrollY() > 0; - } - } else { - return ViewCompat.canScrollVertically(mTarget, -1); - } - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - ensureTarget(); - - final int action = MotionEventCompat.getActionMasked(ev); - - if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { - mReturningToStart = false; - } - - if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) { - // Fail fast if we're not in a state where a swipe is possible - return false; - } - - switch (action) { - case MotionEvent.ACTION_DOWN: - setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); - mActivePointerId = MotionEventCompat.getPointerId(ev, 0); - mIsBeingDragged = false; - final float initialMotionY = getMotionEventY(ev, mActivePointerId); - if (initialMotionY == -1) { - return false; - } - mInitialMotionY = initialMotionY; - - case MotionEvent.ACTION_MOVE: - if (mActivePointerId == INVALID_POINTER) { - Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); - return false; - } - - final float y = getMotionEventY(ev, mActivePointerId); - if (y == -1) { - return false; - } - final float yDiff = y - mInitialMotionY; - if (yDiff > mTouchSlop && !mIsBeingDragged) { - mIsBeingDragged = true; - mProgress.setAlpha(STARTING_PROGRESS_ALPHA); - } - break; - - case MotionEventCompat.ACTION_POINTER_UP: - onSecondaryPointerUp(ev); - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mIsBeingDragged = false; - mActivePointerId = INVALID_POINTER; - break; - } - - return mIsBeingDragged; + public boolean start() { + if (!isEnabled()) return false; + if (mRefreshing) return false; + mCircleView.clearAnimation(); + mProgress.stop(); + // See ACTION_DOWN handling in {@link #onTouchEvent(...)}. + setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); + mTotalMotionY = 0; + mIsBeingDragged = true; + mProgress.setAlpha(STARTING_PROGRESS_ALPHA); + return true; } - private float getMotionEventY(MotionEvent ev, int activePointerId) { - final int index = MotionEventCompat.findPointerIndex(ev, activePointerId); - if (index < 0) { - return -1; + /** + * Apply a pull impulse to the effect. If the effect is disabled or has yet + * to start, the pull will be ignored. + * @param delta the magnitude of the pull. + */ + public void pull(float delta) { + if (!isEnabled()) return; + if (!mIsBeingDragged) return; + delta *= DRAG_RATE; + float max_delta = mTotalDragDistance / MIN_PULLS_TO_ACTIVATE; + delta = Math.max(-max_delta, Math.min(max_delta, delta)); + mTotalMotionY += delta; + + // See ACTION_MOVE handling in {@link #onTouchEvent(...)}. + final float overscrollTop = mTotalMotionY; + mProgress.showArrow(true); + float originalDragPercent = overscrollTop / mTotalDragDistance; + if (originalDragPercent < 0) return; + float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); + float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; + float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; + float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset + - mOriginalOffsetTop : mSpinnerFinalOffset; + float tensionSlingshotPercent = Math.max(0, + Math.min(extraOS, slingshotDist * 2) / slingshotDist); + float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( + (tensionSlingshotPercent / 4), 2)) * 2f; + float extraMove = (slingshotDist) * tensionPercent * 2; + + int targetY = mOriginalOffsetTop + + (int) ((slingshotDist * dragPercent) + extraMove); + // where 1.0f is a full circle + if (mCircleView.getVisibility() != View.VISIBLE) { + mCircleView.setVisibility(View.VISIBLE); + } + if (!mScale) { + mCircleView.setScaleX(1f); + mCircleView.setScaleY(1f); + } + if (overscrollTop < mTotalDragDistance) { + if (mScale) { + setAnimationProgress(overscrollTop / mTotalDragDistance); + } } - return MotionEventCompat.getY(ev, index); - } + float strokeStart = (float) (adjustedPercent * .8f); + mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); + mProgress.setArrowScale(Math.min(1f, adjustedPercent)); - @Override - public void requestDisallowInterceptTouchEvent(boolean b) { - // Nope. - } + float alphaStrength = Math.max(0f, Math.min(1f, (dragPercent - .9f) / .1f)); + int alpha = STARTING_PROGRESS_ALPHA; + alpha += (int) (alphaStrength * (MAX_ALPHA - STARTING_PROGRESS_ALPHA)); + mProgress.setAlpha(alpha); - private boolean isAnimationRunning(Animation animation) { - return animation != null && animation.hasStarted() && !animation.hasEnded(); + float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; + mProgress.setProgressRotation(rotation); + setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, + true /* requires update */); } - @Override - public boolean onTouchEvent(MotionEvent ev) { - final int action = MotionEventCompat.getActionMasked(ev); - - if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { - mReturningToStart = false; - } - - if (!isEnabled() || mReturningToStart || canChildScrollUp()) { - // Fail fast if we're not in a state where a swipe is possible - return false; + /** + * Release the active pull. If no pull has started, the release will be + * ignored. If the pull was sufficiently large, the refresh sequence will + * be initiated. + * @param allowRefresh whether to allow a sufficiently large pull to trigger + * the refresh action and animation sequence. + */ + public void release(boolean allowRefresh) { + if (!mIsBeingDragged) return; + + // See ACTION_UP handling in {@link #onTouchEvent(...)}. + mIsBeingDragged = false; + final float overscrollTop = mTotalMotionY; + if (isEnabled() && allowRefresh && overscrollTop > mTotalDragDistance) { + setRefreshing(true, true /* notify */); + return; } + // cancel refresh + mRefreshing = false; + mProgress.setStartEndTrim(0f, 0f); + Animation.AnimationListener listener = null; + if (!mScale) { + if (mCancelAnimationListener == null) { + mCancelAnimationListener = new Animation.AnimationListener() { - switch (action) { - case MotionEvent.ACTION_DOWN: - mActivePointerId = MotionEventCompat.getPointerId(ev, 0); - mIsBeingDragged = false; - break; - - case MotionEvent.ACTION_MOVE: { - final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); - if (pointerIndex < 0) { - Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); - return false; - } - - final float y = MotionEventCompat.getY(ev, pointerIndex); - final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; - if (mIsBeingDragged) { - mProgress.showArrow(true); - float originalDragPercent = overscrollTop / mTotalDragDistance; - if (originalDragPercent < 0) { - return false; - } - float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); - float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; - float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; - float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset - - mOriginalOffsetTop : mSpinnerFinalOffset; - float tensionSlingshotPercent = Math.max(0, - Math.min(extraOS, slingshotDist * 2) / slingshotDist); - float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( - (tensionSlingshotPercent / 4), 2)) * 2f; - float extraMove = (slingshotDist) * tensionPercent * 2; - - int targetY = mOriginalOffsetTop - + (int) ((slingshotDist * dragPercent) + extraMove); - // where 1.0f is a full circle - if (mCircleView.getVisibility() != View.VISIBLE) { - mCircleView.setVisibility(View.VISIBLE); + @Override + public void onAnimationStart(Animation animation) { } - if (!mScale) { - ViewCompat.setScaleX(mCircleView, 1f); - ViewCompat.setScaleY(mCircleView, 1f); - } - if (overscrollTop < mTotalDragDistance) { - if (mScale) { - setAnimationProgress(overscrollTop / mTotalDragDistance); - } - if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA - && !isAnimationRunning(mAlphaStartAnimation)) { - // Animate the alpha - startProgressAlphaStartAnimation(); - } - float strokeStart = (float) (adjustedPercent * .8f); - mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); - mProgress.setArrowScale(Math.min(1f, adjustedPercent)); - } else { - if (mProgress.getAlpha() < MAX_ALPHA - && !isAnimationRunning(mAlphaMaxAnimation)) { - // Animate the alpha - startProgressAlphaMaxAnimation(); + + @Override + public void onAnimationEnd(Animation animation) { + if (!mScale) { + startScaleDownAnimation(mRefreshListener); } } - float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; - mProgress.setProgressRotation(rotation); - setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, - true /* requires update */); - } - break; - } - case MotionEventCompat.ACTION_POINTER_DOWN: { - final int index = MotionEventCompat.getActionIndex(ev); - mActivePointerId = MotionEventCompat.getPointerId(ev, index); - break; - } - case MotionEventCompat.ACTION_POINTER_UP: - onSecondaryPointerUp(ev); - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: { - if (mActivePointerId == INVALID_POINTER) { - if (action == MotionEvent.ACTION_UP) { - Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); - } - return false; - } - final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); - final float y = MotionEventCompat.getY(ev, pointerIndex); - final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; - mIsBeingDragged = false; - if (overscrollTop > mTotalDragDistance) { - setRefreshing(true, true /* notify */); - } else { - // cancel refresh - mRefreshing = false; - mProgress.setStartEndTrim(0f, 0f); - Animation.AnimationListener listener = null; - if (!mScale) { - listener = new Animation.AnimationListener() { - - @Override - public void onAnimationStart(Animation animation) { - } - - @Override - public void onAnimationEnd(Animation animation) { - if (!mScale) { - startScaleDownAnimation(null); - } - } - - @Override - public void onAnimationRepeat(Animation animation) { - } - - }; + @Override + public void onAnimationRepeat(Animation animation) { } - animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); - mProgress.showArrow(false); - } - mActivePointerId = INVALID_POINTER; - return false; + }; } + listener = mCancelAnimationListener; } + animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); + mProgress.showArrow(false); + } - return true; + /** + * Reset the effect, clearing any active animations. + */ + public void reset() { + mIsBeingDragged = false; + setRefreshing(false, false /* notify */); + mProgress.stop(); + mCircleView.setVisibility(View.GONE); + setColorViewAlpha(MAX_ALPHA); + // Return the circle to its start position + if (mScale) { + setAnimationProgress(0 /* animation complete and view is hidden */); + } else { + setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop, + true /* requires update */); + } + mCurrentTargetOffsetTop = mCircleView.getTop(); } private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { @@ -875,17 +742,19 @@ public class SwipeRefreshLayout extends ViewGroup { if (isAlphaUsedForScale()) { mStartingScale = mProgress.getAlpha(); } else { - mStartingScale = ViewCompat.getScaleX(mCircleView); + mStartingScale = mCircleView.getScaleX(); + } + if (mScaleDownToStartAnimation == null) { + mScaleDownToStartAnimation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); + setAnimationProgress(targetScale); + moveToStart(interpolatedTime); + } + }; + mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); } - mScaleDownToStartAnimation = new Animation() { - @Override - public void applyTransformation(float interpolatedTime, Transformation t) { - float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); - setAnimationProgress(targetScale); - moveToStart(interpolatedTime); - } - }; - mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); if (listener != null) { mCircleView.setAnimationListener(listener); } @@ -902,17 +771,6 @@ public class SwipeRefreshLayout extends ViewGroup { } } - private void onSecondaryPointerUp(MotionEvent ev) { - final int pointerIndex = MotionEventCompat.getActionIndex(ev); - final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); - if (pointerId == mActivePointerId) { - // This was our active pointer going up. Choose a new - // active pointer and adjust accordingly. - final int newPointerIndex = pointerIndex == 0 ? 1 : 0; - mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); - } - } - /** * Classes that wish to be notified when the swipe gesture correctly * triggers a refresh should implement this interface. diff --git a/ui/android/java/res/drawable-hdpi/refresh_blue.png b/ui/android/java/res/drawable-hdpi/refresh_blue.png deleted file mode 100644 index 503e364e494758bf118fdbc78d0608390d0a5753..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcwPel00001 literal 1436 zcwUv1c{tPw7{|w%Cii`WNwIlWq*}4*FvyHbrEx{Vxb3(S%8|wzIdUsU2tjg3qull@%)0Pvb) zV`DtBSS;>$>FDSn2of3^y0o;^)YLRSKEAQB;pgX9UtiC&)z;QhD3qF-nudl3e}8|T zA0Gz{Oe0ypLt7xq-a>CY0=x@A7(oM{ftHDYLnbghg8&fvZU(uU0*uTerCqRH2EZ7F zty2LIM)O*ro(aS?7ko7cznwr0 zbkH;oxK{~r0MtDXyk7-TtDvD7WR8Uf6oZ|AA#S-~ZZovDiMEU(A!Q(~2D+F8ke`5E zOoW&S{89uyZi0Fz5eP=}TOp$J&~e^Hj6$rfu3`B2g#{({?GaLz7uYK!zF)Lj3M~5n-!pIVTSe%**{M+q%7bkaMEWHGb%ry_mh8ox}0IsN`@AM!=6~WmEqhC*k9yps}*wHgo7ZWyw3RfjxjyY~x}aZgX$Uhbnp zK7wXYwW374;!RZz?MT|)Lz4ND1J~TTDG>&Blrz%(b$x;wE+Ln$%wh$f>OEQyKQ7kw z#dB3x;g&A{0KMQ?s$j=9((D+|&~joAZK`EjEPhbZ$_iKcAYrlFxfz)H+8#gVO~I`k z{u)rGQl;x;B-=p3WZKWi`P6d#( zku0(j)-|=`1WeYHvb`8|9lKd|e%UQNY#Vs7gL%L`0v*17XSZ6aE6^$M*^0h+{|xT% z=WOiNbaKG*Oa!|Ho+;TaQHjUtYS9-z!BI)%sQHPMBU>w%9b?xwSZ_`(PKEXaxA`p@ zh!ZL*hO2sC}iQ1!HdORt(qn(vW(&PF}sf}x~* zZxYk5t3&sb^p54ahp+0BVuipn?b{yjeOIQlO|Xn&BYO&UQsl+Hc|eX|J3T#HiQe? zgcJH6mOsuV zrrhKPcE(=!@c_=dKbL=zeA}a)m(O z@`P%sL#9*F>90+vB%GM7a}26qd*spm6Wk3M+G>$NqIwmhz02J7+jAtETbWhkU8w&6 DSp3j} diff --git a/ui/android/java/res/drawable-hdpi/refresh_gray.png b/ui/android/java/res/drawable-hdpi/refresh_gray.png deleted file mode 100644 index 486b419114beead58808d2f98e9f10ea9b2d0bfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcwPel00001 literal 1208 zcwPax1V{UcP)<8Y&K@qq!MK?_GtLz@-v^zU`{Tp;aqhW@@pJmf`p>+50!ci=04oKN zetyJX5)X_d04WS2jVyBb0eRLF7XRg^9KTKT*OUM~1cXL-%v%kifD+0WMHOSLan=~C z%5Td2rNASyJd$xN!U)75GQ2^Nx2n!gZY&(Go~*ge%T~wjs;=s|mo2w>vUa$-u`oNy zNM#;5#4`r1!FUi^HvKRvOVj%+ZlkLd`Kznk#{SCEG%7qJkE{n1(-ujjkmlt^P+OmC zZ>eiVA$8r}TAxFWXXbf@6q5J1i=%CkW3N}=JacyyMVGt#dLC6?A*b!)+|MNg+7{*I znbTK_?yu8V%QGlzy9`8ik)ts(D4?>rcy*yDo~|x77g6EWG8)B+VvQ8CD57@oLft5` zpqqmisPT$fUO5pBqck;fvZlzMPS&O-PzsHrox&K;pMKj@WLMwzo3c1k=4p~gF;&B>g^Ao8eih|hh3^mXLLkO zdQWfZN7|)7sNHwmC!hIz28|#qHo0SuTDYU_ijL_g+WwCk)Z%Zw7~8(bA!=fi9(!ak z{Bk~&S*cFjeqzAC7)CHy%11BfF^r7ZMDk?_mG)M!Lq6Ww_#J&<$Gq!#w?kh9!ylx* zg$jmxb0>+%C@xK_>mZDSTig9gpQ$6?{${r{Z+-nB|M|C0+{G1n=VWi7miJeJ>><`* z8=UnW@s-}om_QKPUqShHz&hG7Rkslk8?y9oItu!$L%)OuaT};==QxrvD9lcFl{r)D zf}xE16dI(fxZ^KK<{$~Ks2dCB<&VWFSK%?tAgqlAU17Z_(GhaEY7A4U8|M)*%pixW zUX*wpNDiasga1r2B^m}Rs3{mljtLwMqC>T5nsDST`Z+R&8N_YsaP>e^$YboXWtbop zq7xaz4074R81J6C1=4X16TIqBH^k6|uarR@S3q=me}Zvy^1js|d!#I@t1!;HbD94i zfkbzWJ&9{91Zylwu*OYagT7N+>wC!bwNTK^Harhm(Cn6Y(YZIePTkNY-twWx;t>sR z^q-`{A9}XifPcg-b%73U18g@&jPbLK`yy|H8zalD;yE>B%tv_}+$vgZo;!5f()J{SLj#c1Z(5UtP21&|MvZ-PxqH8JHJIu`QqF2al@VXEHW#3RIi)$!)2pWSP+un|_al!8%`? z7sP9oaj872e6!?8g3TIU=>-=ht~s3xb9I`r?N%LwH%r}a*TO^PcdGI)w2GFn@7?uU z=k}Z*p%I=})$e?{sCGZ9J>=T3PQacAr0TXOA9?%nmqvZFRF) z@JGY^@`pmZa{I))pF1m`y7%JQ!Iw{7o-CelU}2Z=v$;nfnf@(_J9mV2@=NVZ?$Wre z-A@I(%M_kH4C&f^efj0|{-^J|ix-wixbd9b-kMRW^mg_4;|i5Uo3vFn-c$H|_TX8j zBQo5D-=%bRl)GMEcwtKBgy@$INjAdUlcmqKE-cs5n{@H8l7^epor^ia`fWyg9#8dN zU?KF|+~oEZ@6_`XtzI@iaLPv0)qu*m=4%PA}iEJXVirZ!yIx$kfNFSCW3i6v(Y zPWoT1`1|a`nLk&axMl4TeKdFD#Ak+YC6BYbSATT)hQ+coNgTe~ HDWM4fvj6B? diff --git a/ui/android/java/res/drawable-mdpi/refresh_gray.png b/ui/android/java/res/drawable-mdpi/refresh_gray.png deleted file mode 100644 index 2c9ebc3d143d855b4e41f87880e15c609f85084e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcwPel00001 literal 757 zcwPbZ0t)?!P)B`?B-yZ!(hAcBl}Z*3fe2dFMJNkW z3?>VvM;@#?9giEk&Dj`V?r%ot{xMeiSyUw@i~9Rt46R}vHfq>Jo!aD+%{Ns>${!J9 zZQ>ee!o`0O*rf!YT)t^AV#i060@vVR2f{lTb%XxQpHHJO3g^?AAM{6E?*M{P2eo1c zSXhJ2G`G{9^d80wnXQNMq}OS~g#()z)@tY~>exQoAA3>cf6n;F$V<11Ll z7FxsOr+WiX)6@O%7%gV4qmqx_fCDl17Ox9gK(AvDf|(m;yv4Jf!@b~SA&Vv}lMdFPhULWleY6Mss+qye_R*mbMA|cjv2cj`mmY5Oe3S zF&$Kw?_Gmywj`2)9LX~|l<(xZyphkT*p+ULOIl^=-kr8#WC-@N5&jP;?lK?N|CG|-T-~PUh(E}}K(!&mkR$@vWB727@^n3Y5Mk$kSh#n-$ z*f|g+RuWSh-a)n@1L}kPp+)8?yq?F6*-*w<*g$jC%_^fG-&#j2ug&mL7fn(v<+vO4 zP51t4M4RD3A1dPo+h)R$DX&j>|=J7AHxSnZ<+AOzvhPJrkl{@LJvT{Gmj|xm(XQ8!~Tl8Z= zooa>DJ{E;PbCl1gZ#fP6bFgZTh7~kTn-(-|6w$OfN5h6F4V%z3Z~W80+k)BQx;us9 n4j1KJMcQ_9>EAu4>A(U2zv8ub&iv2e00000NkvXXu0mjfUl(X^ diff --git a/ui/android/java/res/drawable-xhdpi/refresh_blue.png b/ui/android/java/res/drawable-xhdpi/refresh_blue.png deleted file mode 100644 index a77a5512a7ef0ad6ace3fb7fcd86b07cd702dbdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcwPel00001 literal 1941 zcwV(t`#;kS8(vJjJ&7Dr#EOy*NFT3tU=EQ(LRhD*9;wKaq|$P%?0F~SdFE76M93kB zoML2a8ZnB@EKQ@yMk|Zi_pmR~yXXBM-s|(Zulw-1?(2v9mzx^k=c&EQcohnT()RXp z4^(C1KcS_r;$b`UKTxO@ivhmD9;p8Vm`tXM0RT|3TrPKTa5#7F+`_^F1VQ%p_7neu zY7T;6U0t16ELJL&R#sLmEiI~*@87=@iNv|Nxq*R!`T2R3gGy#$VWHBf>bST#l@FWE z=5o0_9&d7TQYMonB_-wL$UzDqnL!3WE8SCoZ*#~TfPDFecvAsbiAZOW9GC6X!6a*lost0~Gto-@|nf`%@ zqzIFz49Nq|=7Al2WhomT`-=2^Qr;*Bua$z$18{sL*g2w9BFLyjIW~=Cw?eg&W%%2wwK{7sx@IODjg2noiA_IR+99*sN{R&1lhq2#GhZ0C{Yjq;rnbn} z?^}+zLn(c8!0o`MuF_=6OQob1h0;KIySoO*uBaJ`@UM0;=)PJJ9kOzLifLaU@x_nP z4}Ta{Q`0RKORPaBdn}eqXmI&8yD&A96aMj%?nyfRQvlwe47UC0%Atjv*T(bhA?%SL zy!tP4=D;4danp^F;$foav&nA`yx`It)`N$#{pFZA6wGFKIo&Zr$|px7W%M$9KPf^t znG-u|JGndYwoYg3OogMquEuclzYP)6iyvsS6ATPy+%f4^fi^9JM$-((6!~NCUM(Vs zG-$7;2!q460k~d&7$c#Z!Tt(;;I|?NiTC@;i=6DaFY`XjgM5ic-uVFep$z-IpGai3 z&-2-+3zz9PN2qu-jVX~|JRY84?i7p5Ws#N>GgsB^zhz>&?d)@%^t~8Pt+80HmY93l zPW!d@#Vy(NEa6M;VRQ2|UgqzY3%2Y$eqHbcsd^V}JpBFg5p44Nl*Y528#IGP0_vL8 zMU&J>;rVx)D|yvNe7$mm1SQfv8cnClKio>ntc{PYs`J1WNp#ul#0fhryRLY9A!L3Ah&~n*Imhcxqy&sQ={k&8+MCsI#OpvUovL|)=)+p z#vB$0r~Z<1#LS5Nj0xH1+Po#VGZEcQ8M2(tw}Kv{I*zFQk-%1L$fD_gUCk^!At*V~ zYMq>aAjWxdtLe^Dl^P8_o~cP{Qk%mf>&@1twxpH!`oICmQu zj|J2^%q0Wa1%Hc}+I-wZjE_HSoT_CIp{Ni?qOk{VIB(tV5ywA+cHUMP#SyF@KYqb; z!9iZ6Ub!Z)d}CIVU82j+$~)->RiP3?n!F~c zJ4qWad6pzExCb%{t`!ZG3CUrkKSQ}G*kqGSYPj$!ub`E}bM|DDwJV&<(O2vpDR+&8 z)?Pbnyx7=vQKM@?1zNb{%hEJqTr8w&?^pro1-hPkh zP!I}Ix9@D*(N7jX8^qvFmyXIG2N5R>t+&J-Dbii_omUv=wWOrw*Wj!F zX~8v&Wv@MTSxLv;KFf?z9zQ{-LFzttc+|JdvH3+eCqj|sPW(UzyR3c(ZJGEBT+|8p z6WqR+Q>HBa81$V-DnDhrY z`e?u|eQc_t(9kgzEt%1gLZrM9@t~xwaM_4$u6x=~dRd+2y VG&K}6yZ`xz-X4DL&)q`E{{_p20l5GG diff --git a/ui/android/java/res/drawable-xhdpi/refresh_gray.png b/ui/android/java/res/drawable-xhdpi/refresh_gray.png deleted file mode 100644 index ceb1321b1d5e16671cb460d8adfe630b604c9bde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcwPel00001 literal 1683 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 diff --git a/ui/android/java/res/drawable-xxhdpi/refresh_gray.png b/ui/android/java/res/drawable-xxhdpi/refresh_gray.png deleted file mode 100644 index 365e2feda07ebb79d2f1a99bbdcaaed3e9fbf133..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcwPel00001 literal 2633 zcwV)Z_gmA;5{DxMr7FFNNKGOVq)AbZKnM`3NI4=%Q#t}3L4kObUIIsu8mbbif&yZr zDK%oKA<{w-5=wxC5<*Aee$Tys!`Akc{yHkS6rp#SsO+uQq}b8v9b(b18SkN1< zxVT88(T0YGc6N3)H#Z#}9k;i)fjm&t)YJqxv$M0Ip`pN*h=@pjetunD-TwZ5d3iaV zPKU$cKygJyMMOlz*4EbQ>M9fp1^QB{)Y8&YH#fKC<>mSL`ETF8t^Rj3HZ}qwkOUYe zCnte>fKxy?FfcGWI@;6IGcz-@va&)T5C9mUW_NeDx3`x>BK7z8GZ>7)!9f6{tE&q@ z0`LH4K*I3wFp)^??Cb>k;_>*YsVSfxpciPjwzf7tJ`Sj+P$&Q`lgXTzm;m$wcq1bt z-QC?ke*7Sl$5+FILzPO!6`YqH`;_e>%@2Ks-e9@N%Hu8`E`_Io`K3v|a7%$#;? zzrYsluFm73%-1#=4X=VbI`33A_6rbZt3V)54l|^oefWu~EOHPPB_5wB(K1_p4;$Fh z&=TkJC9Fx7UoZniE=6L(U!&>!)f+I$3%op9Vyx)~75$=+r^n3W;*R}`l^wbb$eInG zRM-)#C;N0|yCU+2aNJ5OKRDg_ub1o*_c2cWs-CxU9?bfQf0PtcnofwQ(~MKfd@j|k zDS~I|@kFJSnwH@8FR4DL8N;_Cn$ z-Ow(+XI?IW0m~WGBgyFcM@AlOW1hBEx?P`{G&Os!es;!IiVU+%2m}jy$l=b#CLWFN z@5CNK^}jxpeprWDeNHe?zo%i0>@c1pe)G?o`lQlI zTyf;vO8k7aNi8h+47!+lMS;D50$-KD$mZn)YoxQzHYsY;EWho*?bLU;5^CeZO7Spa zGfxwHN{w`};~adoCHHSdNNB9wZz^;;{iPT>u2@mStuGal08V$$mYUIC9?)D*!z9b* zwv?Uomb3TjX5CUK`e)5})T4dxh5Stpt=zX(j=D~7G9q5CSw&h8JKk0N3ma&qAk<^F zZD_(Hb9U)MmZ$k-V-$uHk{qC2!mKmn!Rbnw@=ZFl8H2HK_2~8v%a|K!q9(z@ZSL+Z z1+>i|#Dtxb%nz-IF!FHOH%oiYWhE{BHG99*M;n}zvl}es> zZ{8&Gaikk&=9q^V@E7#S%#%tZOe`P;raJaz4mZKL`Ry&g6qJkYA>C1u{OjacoAGy}$%Wt0db0(oLeI3C zT(Ld<*rS}!3h*x(;d>-29zZ;t@h8#AR)v7o6J8(Lb z9dUJ|^_ut8vh~}tOPvEnmN4<>{)l7o9r}!4tp6Bcs|Sk7jQP}~y2Ed+OZ%HH&XM1n zZzhUTpHM12N6Io1aeaw;?5s_k(lDfeP|{mG;OW3s0asa^TuFxbiTuK%aiK^^Sq}deV&%O2FNSXQZZKa+xQ473*$cu>!GW$WbqB9U)SVefy4ScvBe~Tzl?Y&N!)!)^j_%57# zbyp|m#KEUa7&VK6y_*uKxVbc?aEnKf#3;EzUMbx|@2ls%KrT+2b1wRb+fg4F!SOCoR0#V8~1Q21H9oCB9nJ!bg#%>@KZh?~0)y z@5fz%{;3U7WMO%0>0k8xSbg_V{Op-W=SW1lYLtBREJMF~kwh}!2UGNy%yNx+=PC0Z zs#k>6C2tRHzum6oFlr}Z*NmRfuC1=&R;qvR`J~vO_>oZU95*@9BH?HfWt3NW|GT_U~V$$2Q^s9 zhyA@^!E{OVh(7ae4Zx$1pNyEyb5m0u4o%(HF8k2idk0d*jSbPz_B9pHxFTLCVDg7my5E^t z4J*Q_tD9e=+9epn8W6(n(3?RCyQ^y%4+=Y}sm&dP0>+H%)7iVw;OR6_YvO}HTo$Ky za7Kl#-fQDvcUS_>9eb;7r diff --git a/ui/android/java/res/drawable-xxxhdpi/refresh_blue.png b/ui/android/java/res/drawable-xxxhdpi/refresh_blue.png deleted file mode 100644 index 3198626ac8cd26ba2a7f10aeb6b1f4331023dfa3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcwPel00001 literal 3834 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 AkN^Mx diff --git a/ui/android/java/res/drawable-xxxhdpi/refresh_gray.png b/ui/android/java/res/drawable-xxxhdpi/refresh_gray.png deleted file mode 100644 index 7ca667846d9882061074139004de4a8f40b3ae42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcwPel00001 literal 3446 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 Wqa&*N{;U6a+vsT