package org.wikipedia.views;
import android.content.Context;
import android.graphics.Canvas;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.webkit.WebView;
import org.wikipedia.WikipediaApp;
import org.wikipedia.events.WebViewInvalidateEvent;
import org.wikipedia.util.DimenUtil;
import java.util.ArrayList;
import java.util.List;
public class ObservableWebView extends WebView {
private static final WebViewInvalidateEvent INVALIDATE_EVENT = new WebViewInvalidateEvent();
private List<OnClickListener> onClickListeners;
private List<OnScrollChangeListener> onScrollChangeListeners;
private List<OnDownMotionEventListener> onDownMotionEventListeners;
private List<OnUpOrCancelMotionEventListener> onUpOrCancelMotionEventListeners;
private List<OnContentHeightChangedListener> onContentHeightChangedListeners;
private OnFastScrollListener onFastScrollListener;
private int contentHeight = 0;
private float touchStartX;
private float touchStartY;
private int touchSlop;
private long lastScrollTime;
private int totalAmountScrolled;
/**
* Threshold (in pixels) of continuous scrolling, to be considered "fast" scrolling.
*/
private static final int FAST_SCROLL_THRESHOLD = (int) (1000 * DimenUtil.getDensityScalar());
/**
* Maximum single scroll amount (in pixels) to be considered a "human" scroll.
* Otherwise it's probably a programmatic scroll, which we won't count.
*/
private static final int MAX_HUMAN_SCROLL = (int) (500 * DimenUtil.getDensityScalar());
/**
* Maximum amount of time that needs to elapse before the previous scroll amount
* is "forgotten." That is, if the user scrolls once, then scrolls again within this
* time, then the two scroll actions will be added together as one, and counted towards
* a possible "fast" scroll.
*/
private static final int MAX_MILLIS_BETWEEN_SCROLLS = 500;
public void addOnClickListener(OnClickListener onClickListener) {
onClickListeners.add(onClickListener);
}
public void addOnScrollChangeListener(OnScrollChangeListener onScrollChangeListener) {
onScrollChangeListeners.add(onScrollChangeListener);
}
public void addOnDownMotionEventListener(OnDownMotionEventListener onDownMotionEventListener) {
onDownMotionEventListeners.add(onDownMotionEventListener);
}
public void addOnUpOrCancelMotionEventListener(OnUpOrCancelMotionEventListener onUpOrCancelMotionEventListener) {
onUpOrCancelMotionEventListeners.add(onUpOrCancelMotionEventListener);
}
public void addOnContentHeightChangedListener(OnContentHeightChangedListener onContentHeightChangedListener) {
onContentHeightChangedListeners.add(onContentHeightChangedListener);
}
public void setOnFastScrollListener(OnFastScrollListener onFastScrollListener) {
this.onFastScrollListener = onFastScrollListener;
}
public void clearAllListeners() {
onClickListeners.clear();
onScrollChangeListeners.clear();
onDownMotionEventListeners.clear();
onUpOrCancelMotionEventListeners.clear();
onContentHeightChangedListeners.clear();
onFastScrollListener = null;
}
public interface OnClickListener {
boolean onClick(float x, float y);
}
public interface OnScrollChangeListener {
void onScrollChanged(int oldScrollY, int scrollY, boolean isHumanScroll);
}
public interface OnDownMotionEventListener {
void onDownMotionEvent();
}
public interface OnUpOrCancelMotionEventListener {
void onUpOrCancelMotionEvent();
}
public interface OnContentHeightChangedListener {
void onContentHeightChanged(int contentHeight);
}
public interface OnFastScrollListener {
void onFastScroll();
}
public void copyToClipboard() {
// Simulate a Ctrl-C key press, which copies the current selection to the clipboard.
// Seems to work across all APIs.
dispatchKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_C, 0, KeyEvent.META_CTRL_ON));
dispatchKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
KeyEvent.ACTION_UP, KeyEvent.KEYCODE_C, 0, KeyEvent.META_CTRL_ON));
}
public ObservableWebView(Context context) {
super(context);
init();
}
public ObservableWebView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ObservableWebView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
@Override public void destroy() {
clearAllListeners();
if (getParent() != null) {
((ViewGroup) getParent()).removeView(this);
}
super.destroy();
}
private void init() {
onClickListeners = new ArrayList<>();
onScrollChangeListeners = new ArrayList<>();
onDownMotionEventListeners = new ArrayList<>();
onUpOrCancelMotionEventListeners = new ArrayList<>();
onContentHeightChangedListeners = new ArrayList<>();
touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
@Override
protected void onScrollChanged(int left, int top, int oldLeft, int oldTop) {
super.onScrollChanged(left, top, oldLeft, oldTop);
boolean isHumanScroll = Math.abs(top - oldTop) < MAX_HUMAN_SCROLL;
for (OnScrollChangeListener listener : onScrollChangeListeners) {
listener.onScrollChanged(oldTop, top, isHumanScroll);
}
if (!isHumanScroll) {
return;
}
totalAmountScrolled += (top - oldTop);
if (Math.abs(totalAmountScrolled) > FAST_SCROLL_THRESHOLD
&& onFastScrollListener != null) {
onFastScrollListener.onFastScroll();
totalAmountScrolled = 0;
}
lastScrollTime = System.currentTimeMillis();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
for (OnDownMotionEventListener listener : onDownMotionEventListeners) {
listener.onDownMotionEvent();
}
if (System.currentTimeMillis() - lastScrollTime > MAX_MILLIS_BETWEEN_SCROLLS) {
totalAmountScrolled = 0;
}
touchStartX = event.getX();
touchStartY = event.getY();
break;
case MotionEvent.ACTION_UP:
if (Math.abs(event.getX() - touchStartX) <= touchSlop
&& Math.abs(event.getY() - touchStartY) <= touchSlop) {
for (OnClickListener listener : onClickListeners) {
if (listener.onClick(event.getX(), event.getY())) {
return true;
}
}
}
case MotionEvent.ACTION_CANCEL:
for (OnUpOrCancelMotionEventListener listener : onUpOrCancelMotionEventListeners) {
listener.onUpOrCancelMotionEvent();
}
break;
default:
// Do nothing for all the other things
break;
}
return super.onTouchEvent(event);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isInEditMode()) {
return;
}
if (contentHeight != getContentHeight()) {
contentHeight = getContentHeight();
for (OnContentHeightChangedListener listener : onContentHeightChangedListeners) {
listener.onContentHeightChanged(contentHeight);
}
}
WikipediaApp.getInstance().getBus().post(INVALIDATE_EVENT);
}
}