package com.orgzly.android.ui.views;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.AdapterView;
import android.widget.ListView;
import com.orgzly.BuildConfig;
import com.orgzly.R;
import com.orgzly.android.util.LogUtils;
import java.util.HashMap;
public class GesturedListView extends ListView implements GestureDetector.OnGestureListener {
private static final String TAG = GesturedListView.class.getName();
private int minFlingVelocity;
private int maxFlingVelocity;
// private int touchSlop;
private GestureDetector gestureDetector;
private Drawable selector;
private GesturedListViewItemMenus itemMenus;
private boolean scrolledHorizontally;
private boolean scrolledVertically;
private boolean isItemToolbarActive;
private int itemPosition;
public GesturedListView(Context context) {
super(context);
init(null);
}
public GesturedListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public GesturedListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public GesturedListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs);
}
private void init(AttributeSet attrs) {
maxFlingVelocity = ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity();
minFlingVelocity = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity();
// touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
gestureDetector = new GestureDetector(getContext(), this);
selector = getSelector();
int menuContainerId = 0;
HashMap<Gesture, Integer> gestureMenuMap = new HashMap<>();
/* Get attributes from XML. */
if (attrs != null) {
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.GesturedListView);
menuContainerId = typedArray.getResourceId(R.styleable.GesturedListView_menu_container, 0);
int child;
child = typedArray.getInt(R.styleable.GesturedListView_menu_for_fling_left, -1);
if (child != -1) {
gestureMenuMap.put(Gesture.FLING_LEFT, child);
}
child = typedArray.getInt(R.styleable.GesturedListView_menu_for_fling_right, -1);
if (child != -1) {
gestureMenuMap.put(Gesture.FLING_RIGHT, child);
}
typedArray.recycle();
}
/* Disable selector. */
// setSelector(android.R.color.transparent);
itemMenus = new GesturedListViewItemMenus(this, gestureMenuMap, menuContainerId);
}
public GesturedListViewItemMenus getItemMenus() {
return itemMenus;
}
public void setOnItemMenuButtonClickListener(OnItemMenuButtonClickListener listener) {
itemMenus.setListener(listener);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// if (BuildConfig.LOG_DEBUG) Dlog.method(TAG, friendlyMotionEvent(ev));
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean performItemClick(View view, int position, long id) {
if (! isItemToolbarActive) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, position, id);
return super.performItemClick(view, position, id);
} else {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Ignoring click as toolbar is active", position, id);
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, ev);
// if (true) return super.onTouchEvent(e);
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, friendlyMotionEvent(ev));
/* Remember item on first touch down.
* Any potential gesture will be applied to this item.
*/
itemPosition = this.pointToPosition((int) ev.getX(), (int) ev.getY());
/* Reset flags. */
scrolledHorizontally = false;
scrolledVertically = false;
isItemToolbarActive = false;
// itemMenus.closeAll();
// setEnabled(true);
setSelector(selector);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_SCROLL:
break;
}
gestureDetector.onTouchEvent(ev);
boolean r = super.onTouchEvent(ev);
/* Enable list only after calling super, as we don't want click feedback
* if menu has been opened by this gesture.
*/
if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, friendlyMotionEvent(ev));
scrolledHorizontally = false;
scrolledVertically = false;
// setEnabled(true);
// setSelector(selector);
}
return r;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, friendlyMotionEvent(ev));
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onDown(MotionEvent ev) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, friendlyMotionEvent(ev));
return false;
}
@Override
public void onShowPress(MotionEvent ev) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, friendlyMotionEvent(ev));
// scrolledVertically = true;
}
@Override
public boolean onSingleTapUp(MotionEvent ev) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, friendlyMotionEvent(ev));
return false;
}
@Override
public boolean onScroll(MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
// if (BuildConfig.LOG_DEBUG) Dlog.method(TAG, distanceX, distanceY);
if (Math.abs(distanceX) > Math.abs(distanceY)) {
if (! scrolledVertically) {
if (! scrolledHorizontally) {
scrolledHorizontally = true;
// setEnabled(false);
setSelector(android.R.color.transparent);
if (BuildConfig.LOG_DEBUG)
LogUtils.d(TAG, "Horizontal scroll detected and no vertical, ListView disabled");
} else {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Horizontal scroll detected and no vertical, ListView was already disabled");
}
} else {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Horizontal scroll detected, but so was vertical");
}
} else if (Math.abs(distanceX) < Math.abs(distanceY)) {
scrolledVertically = true;
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Vertical scroll detected");
}
return false;
}
@Override
public void onLongPress(MotionEvent ev) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, friendlyMotionEvent(ev));
}
@Override
public boolean onFling(MotionEvent ev1, MotionEvent ev2, float velocityX, float velocityY) {
if (isItemToolbarActive) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Quick-menu already active");
return false;
}
/* Only if we never scrolled vertically. */
if (scrolledHorizontally) {
int horizontalFling = isHorizontalFling(velocityX, velocityY);
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "After scrolling horizontally: " + horizontalFling);
if (horizontalFling != 0) {
isItemToolbarActive = true;
/* INVALID_POSITION can happen after swiping empty space below notes
* (when there are only few on the top).
*/
if (itemPosition != AdapterView.INVALID_POSITION) {
Gesture gesture = horizontalFling == 1 ?
Gesture.FLING_RIGHT : Gesture.FLING_LEFT;
itemMenus.open(itemPosition, gesture);
/*
* If it's the last item in the list scroll to it to make quick-menu visible.
* Wait for quick-menu opening animation to end.
*/
if (itemPosition == getCount() - 1) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
setSelection(itemPosition);
}
}, getResources().getInteger(R.integer.item_menu_animation_duration));
}
}
return true;
}
} else {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "After not scrolling horizontally");
}
return false;
}
/**
* @return -1 for left fling, 1 for right fling, 0 if the fling is not horizontal
*/
private int isHorizontalFling(float velocityX, float velocityY) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, velocityX, velocityY, minFlingVelocity, maxFlingVelocity);
boolean isHorizontalFLing =
Math.abs(velocityX) > Math.abs(velocityY) && // More horizontal then vertical
Math.abs(velocityX) >= minFlingVelocity && Math.abs(velocityX) <= maxFlingVelocity;
if (isHorizontalFLing) {
return velocityX > 0 ? 1 : -1;
} else {
return 0;
}
}
private String friendlyMotionEvent(MotionEvent ev) {
String action;
if (ev != null) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
action = "ACTION_DOWN";
break;
case MotionEvent.ACTION_UP:
action = "ACTION_UP";
break;
case MotionEvent.ACTION_MOVE:
action = "ACTION_MOVE";
break;
case MotionEvent.ACTION_CANCEL:
action = "ACTION_CANCEL";
break;
case MotionEvent.ACTION_SCROLL:
action = "ACTION_SCROLL";
break;
default:
action = String.valueOf(ev.getAction());
}
} else {
action = "MotionEvent is null (action cannot be taken from it)";
}
return action;
}
public interface OnItemMenuButtonClickListener {
boolean onMenuButtonClick(int buttonId, long itemId);
}
public enum Gesture {
FLING_RIGHT,
FLING_LEFT
}
}