// 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.TraceEvent; import org.chromium.base.metrics.RecordUserAction; import org.chromium.chrome.R; import org.chromium.chrome.browser.tab.Tab; 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. */ 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; // Max allowed duration of the refresh animation after a refresh signal, // guarding against cases where the page reload fails or takes too long. private static final int MAX_REFRESH_ANIMATION_DURATION_MS = 7500; // The modified AppCompat version of the refresh effect, handling all core // logic, rendering and animation. private final SwipeRefreshLayout mSwipeRefreshLayout; // The Tab where the swipe occurs. private Tab mTab; // 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; // Handles removing the layout from the view hierarchy. This is posted to ensure it does not // conflict with pending Android draws. private Runnable mDetachLayoutRunnable; // 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. * @param tab The Tab where the swipe occurs. */ public SwipeRefreshHandler(Context context, Tab tab) { mTab = tab; mContentViewCore = mTab.getContentViewCore(); 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); setEnabled(true); mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { cancelStopRefreshingRunnable(); mSwipeRefreshLayout.postDelayed( getStopRefreshingRunnable(), MAX_REFRESH_ANIMATION_DURATION_MS); if (mAccessibilityRefreshString == null) { int resId = R.string.accessibility_swipe_refresh; mAccessibilityRefreshString = mContentViewCore.getContext().getResources().getString(resId); } mSwipeRefreshLayout.announceForAccessibility(mAccessibilityRefreshString); mTab.reload(); RecordUserAction.record("MobilePullGestureReload"); } }); mSwipeRefreshLayout.setOnResetListener(new SwipeRefreshLayout.OnResetListener() { @Override public void onReset() { if (mDetachLayoutRunnable != null) return; mDetachLayoutRunnable = new Runnable() { @Override public void run() { mDetachLayoutRunnable = null; detachSwipeRefreshLayoutIfNecessary(); } }; mSwipeRefreshLayout.post(mDetachLayoutRunnable); } }); mContentViewCore.setOverscrollRefreshHandler(this); } /** * Destroys and cleans up itself. */ public void destroy() { setEnabled(false); cancelStopRefreshingRunnable(); mSwipeRefreshLayout.setOnRefreshListener(null); mContentViewCore.setOverscrollRefreshHandler(null); } /** * 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; cancelStopRefreshingRunnable(); mSwipeRefreshLayout.postDelayed( getStopRefreshingRunnable(), 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() { cancelStopRefreshingRunnable(); mSwipeRefreshLayout.reset(); } @Override public void setEnabled(boolean enabled) { mSwipeRefreshLayout.setEnabled(enabled); if (!enabled) reset(); } private void cancelStopRefreshingRunnable() { if (mStopRefreshingRunnable != null) { mSwipeRefreshLayout.removeCallbacks(mStopRefreshingRunnable); } } private void cancelDetachLayoutRunnable() { if (mDetachLayoutRunnable != null) { mSwipeRefreshLayout.removeCallbacks(mDetachLayoutRunnable); mDetachLayoutRunnable = null; } } private Runnable getStopRefreshingRunnable() { if (mStopRefreshingRunnable == null) { mStopRefreshingRunnable = new Runnable() { @Override public void run() { mSwipeRefreshLayout.setRefreshing(false); } }; } return mStopRefreshingRunnable; } // The animation view is attached/detached on-demand to minimize overlap // with composited SurfaceView content. private void attachSwipeRefreshLayoutIfNecessary() { cancelDetachLayoutRunnable(); if (mSwipeRefreshLayout.getParent() == null) { mContentViewCore.getContainerView().addView(mSwipeRefreshLayout); } } private void detachSwipeRefreshLayoutIfNecessary() { cancelDetachLayoutRunnable(); if (mSwipeRefreshLayout.getParent() != null) { mContentViewCore.getContainerView().removeView(mSwipeRefreshLayout); } } }