// 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.ntp; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.os.Build; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.widget.ScrollView; import org.chromium.base.Log; import org.chromium.chrome.R; import org.chromium.chrome.browser.widget.FadingShadow; /** * Simple wrapper on top of a ScrollView that will acquire focus when tapped. Ensures the * New Tab page receives focus when clicked. */ public class NewTabPageScrollView extends ScrollView { private static final String TAG = "NewTabPageScrollView"; /** * Listener for scroll changes. */ public interface OnScrollListener { /** * Triggered when the scroll changes. See ScrollView#onScrollChanged for more * details. */ void onScrollChanged(int l, int t, int oldl, int oldt); } private GestureDetector mGestureDetector; private OnScrollListener mOnScrollListener; private NewTabPageLayout mNewTabPageLayout; private FadingShadow mFadingShadow; /** * Constructor needed to inflate from XML. */ public NewTabPageScrollView(Context context, AttributeSet attrs) { super(context, attrs); mGestureDetector = new GestureDetector( getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent e) { boolean retVal = super.onSingleTapUp(e); requestFocus(); return retVal; } }); } /** * Enables drawing a shadow at the bottom of the view when the view's content extends beyond * the bottom of the view. This is exactly the same as a fading edge, except that the shadow * color can have an alpha component, whereas a fading edge color must be opaque. * * @param shadowColor The color of the shadow, e.g. 0x11000000. */ public void enableBottomShadow(int shadowColor) { mFadingShadow = new FadingShadow(shadowColor); setFadingEdgeLength(getResources().getDimensionPixelSize(R.dimen.ntp_shadow_height)); } @Override protected void onFinishInflate() { super.onFinishInflate(); // Incognito also uses this scroll view but will not have the id so will return null. mNewTabPageLayout = (NewTabPageLayout) findViewById(R.id.ntp_content); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mNewTabPageLayout != null) { mNewTabPageLayout.setParentViewportHeight(MeasureSpec.getSize(heightMeasureSpec)); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { mGestureDetector.onTouchEvent(ev); return super.onInterceptTouchEvent(ev); } @Override @SuppressLint("ClickableViewAccessibility") public boolean onTouchEvent(MotionEvent ev) { // Action down would already have been handled in onInterceptTouchEvent if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { mGestureDetector.onTouchEvent(ev); } try { return super.onTouchEvent(ev); } catch (IllegalArgumentException ex) { // In JB MR0 and earlier, an ACTION_MOVE that is not preceded by an ACTION_DOWN event // causes a crash. This can happen under normal circumstances (e.g. going back to the // NTP while a finger is down on the screen) and should not crash. The most reliable way // to prevent this crash is to catch the exception. http://crbug.com/293822 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 && ev.getActionMasked() == MotionEvent.ACTION_MOVE && "pointerIndex out of range".equals(ex.getMessage())) { Log.d(TAG, "Ignoring pointerIndex out of range exception."); return true; } throw ex; } } /** * Sets the listener to be notified of scroll changes. * @param listener The listener to be updated on scroll changes. */ public void setOnScrollListener(OnScrollListener listener) { mOnScrollListener = listener; } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (mOnScrollListener != null) mOnScrollListener.onScrollChanged(l, t, oldl, oldt); } @Override public void focusableViewAvailable(View v) { // To avoid odd jumps during NTP animation transitions, we do not attempt to give focus // to child views if this scroll view already has focus. if (hasFocus()) return; super.focusableViewAvailable(v); } @Override public boolean executeKeyEvent(KeyEvent event) { // Ignore all key events except arrow keys and spacebar. Otherwise, the ScrollView consumes // unwanted events (including the hardware menu button and app-level keyboard shortcuts). // http://crbug.com/308322 switch (event.getKeyCode()) { case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_SPACE: return super.executeKeyEvent(event); default: return false; } } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { // Fixes lanscape transitions when unfocusing the URL bar: crbug.com/288546 outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; return super.onCreateInputConnection(outAttrs); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (mFadingShadow != null) { setVerticalFadingEdgeEnabled(true); float shadowStrength = getBottomFadingEdgeStrength(); float shadowHeight = getVerticalFadingEdgeLength(); setVerticalFadingEdgeEnabled(false); mFadingShadow.drawShadow(this, canvas, FadingShadow.POSITION_BOTTOM, shadowHeight, shadowStrength); } } }