/*
* Copyright (C) 2012 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cyanogenmod.filemanager.ui.widgets;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ListView;
import com.cyanogenmod.filemanager.util.AndroidHelper;
/**
* A {@link ListView} implementation for remove items using flinging gesture.
*/
public class FlingerListView extends ListView {
/**
* An interface for dispatch flinging gestures
*/
public interface OnItemFlingerListener {
/**
* Method invoke when a row item is going to be flinging.
*
* @param parent The AbsListView where the flinging happened
* @param view The view within the AbsListView that was flingered
* @param position The position of the view in the list
* @param id The row id of the item that was flingered
* @return boolean If the flinging operation must continue
*/
boolean onItemFlingerStart(AdapterView> parent, View view, int position, long id);
/**
* Method invoke when a row item was flingered.
*
* @param responder The responder to the flinging action. You MUST be invoke one
* the option methods of this interface (accept or cancel).
* @param parent The AbsListView where the flinging happened
* @param view The view within the AbsListView that was flingered
* @param position The position of the view in the list
* @param id The row id of the item that was flingered
*/
void onItemFlingerEnd(
OnItemFlingerResponder responder,
AdapterView> parent, View view, int position, long id);
}
/**
* An interface for response to {@link OnItemFlingerListener#onItemFlingerEnd(
* OnItemFlingerResponder, AdapterView, View, int, long)} event.
*/
public interface OnItemFlingerResponder {
/**
* Method that indicates that the item was removed. This method MUST be called
* after the remove of item (that it's responsibility of the invoker) to ensure
* that all references are cleaned.
*/
void accept();
/**
* Method that indicates that the action must be cancelled, and the item
* MUST NOT be removed.
*/
void cancel();
}
/**
* An implementation of {@link OnItemFlingerResponder}
*/
private class ItemFlingerResponder implements OnItemFlingerResponder {
/**
* @hide
*/
View mItemView;
/**
* Constructor of
. For synthetic-access only.
*/
public ItemFlingerResponder() {
super();
}
/**
* {@inheritDoc}
*/
@Override
public void accept() {
// Remove the flinger effect
this.mItemView.setTranslationX(0);
clearVars();
}
/**
* {@inheritDoc}
*/
@Override
public void cancel() {
// Remove the flinger effect
this.mItemView.setTranslationX(0);
clearVars();
}
}
/**
* The time after that the pressed is sending to the view.
*/
private static final long PRESSED_DELAY_TIME = 250L;
/**
* The default percentage for flinging remove event.
*/
private static final float DEFAULT_FLING_REMOVE_PERCENTAJE = 0.60f;
/**
* The minimum flinger threshold to start the flinger motion (in dp)
*/
private static final int MIN_FLINGER_THRESHOLD = 16;
// Flinging data
private int mTranslationX = 0;
private int mStartX = 0;
private int mStartY = 0;
private int mCurrentX = 0;
private int mCurrentY = 0;
private int mFlingingViewPos;
private View mFlingingView;
private boolean mFlingingViewPressed;
private int mFlingingViewWidth;
private boolean mScrolling;
private boolean mScrollInAnimation;
private boolean mFlinging;
private boolean mFlingingStarted;
private boolean mMoveStarted;
private boolean mLongPress;
private Runnable mLongPressDetection;
private float mFlingRemovePercentaje;
private float mFlingThreshold;
private OnItemFlingerListener mOnItemFlingerListener;
/**
* Constructor of FlingerListView
.
*
* @param context The current context
*/
public FlingerListView(Context context) {
super(context);
init();
}
/**
* Constructor of FlingerListView
.
*
* @param context The current context
* @param attrs The attributes of the XML tag that is inflating the view.
*/
public FlingerListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* Constructor of FlingerListView
.
*
* @param context The current context
* @param attrs The attributes of the XML tag that is inflating the view.
* @param defStyle The default style to apply to this view. If 0, no style
* will be applied (beyond what is included in the theme). This may
* either be an attribute resource, whose value will be retrieved
* from the current theme, or an explicit style resource.
*/
public FlingerListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* Method that initializes the view
*/
private void init() {
//Initialize variables
this.mFlingRemovePercentaje = DEFAULT_FLING_REMOVE_PERCENTAJE;
this.mFlingThreshold = AndroidHelper.convertDpToPixel(getContext(), MIN_FLINGER_THRESHOLD);
setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mScrollInAnimation = (scrollState == SCROLL_STATE_FLING);
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
}
});
mScrollInAnimation = false;
}
/**
* Method that returns the percentage (from 0 to 1) of the item view width on which
* an OnItemFlinger event occurs
*
* @return float The percentage (from 0 to 1) of the item view width
*/
public float getFlingRemovePercentaje() {
return this.mFlingRemovePercentaje;
}
/**
* Method that sets the percentage (from 0 to 1) of the item view width on which
* an OnItemFlinger event occurs
*
* @param flingRemovePercentaje The percentage (from 0 to 1) of the item view width
*/
public void setFlingRemovePercentaje(float flingRemovePercentaje) {
if (flingRemovePercentaje < 0) {
this.mFlingRemovePercentaje = 0;
} else if (flingRemovePercentaje > 1) {
this.mFlingRemovePercentaje = 1;
} else {
this.mFlingRemovePercentaje = flingRemovePercentaje;
}
}
/**
* Method that sets the listener for listen flinging events
*
* @param mOnItemFlingerListener The flinging listener
*/
public void setOnItemFlingerListener(OnItemFlingerListener mOnItemFlingerListener) {
this.mOnItemFlingerListener = mOnItemFlingerListener;
}
/**
* {@inheritDoc}
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
// If no flinger support is request, don't change the default behaviour
if (this.mOnItemFlingerListener == null) {
return super.onTouchEvent(ev);
}
// This events are trap inside this method
setLongClickable(false);
setClickable(false);
// Get information about the x and y
int x = (int) ev.getX();
int y = (int) ev.getY();
// Detect the motion
int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
// Clean variables
this.mScrolling = false;
this.mFlinging = false;
this.mLongPress = false;
this.mFlingingStarted = false;
this.mMoveStarted = false;
if (this.mFlingingView != null) {
this.mFlingingView.setTranslationX(0);
}
// Get the view to fling
this.mFlingingViewPos = pointToPosition(x, y);
if (this.mFlingingViewPos != INVALID_POSITION) {
this.mStartX = (int) ev.getX();
this.mCurrentX = (int) ev.getX();
this.mStartY = (int) ev.getY();
this.mCurrentY = (int) ev.getY();
this.mTranslationX = 0;
this.mFlingingView =
getChildAt(this.mFlingingViewPos - getFirstVisiblePosition());
this.mFlingingViewPressed = true;
// Detect long press event
if (getOnItemLongClickListener() != null) {
this.mLongPressDetection = new Runnable() {
@Override
@SuppressWarnings({"synthetic-access" })
public void run() {
if (!FlingerListView.this.mFlingingStarted &&
!FlingerListView.this.mMoveStarted) {
// Notify the long-click
FlingerListView.this.mLongPress = true;
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
FlingerListView.this.mFlingingView.setPressed(false);
getOnItemLongClickListener().onItemLongClick(
FlingerListView.this,
FlingerListView.this.mFlingingView,
FlingerListView.this.mFlingingViewPos,
FlingerListView.this.mFlingingView.getId());
}
}
};
this.mFlingingView.postDelayed(
this.mLongPressDetection,
ViewConfiguration.getLongPressTimeout());
}
// Calculate the item size
Rect r = new Rect();
this.mFlingingView.getDrawingRect(r);
this.mFlingingViewWidth = r.width();
// Set the pressed state
this.mFlingingView.postDelayed(new Runnable() {
@Override
@SuppressWarnings("synthetic-access")
public void run() {
if (FlingerListView.this.mFlingingViewPressed) {
FlingerListView.this.mFlingingView.setPressed(true);
return;
}
FlingerListView.this.mFlingingView.setPressed(false);
}
}, PRESSED_DELAY_TIME);
// If not the view is not scrolling the capture event
if (!mScrollInAnimation) {
return true;
}
}
break;
case MotionEvent.ACTION_MOVE:
if (this.mFlingingView != null) {
this.mFlingingView.removeCallbacks(this.mLongPressDetection);
this.mFlingingViewPressed = false;
this.mFlingingView.setPressed(false);
}
// Detect scrolling
this.mCurrentY = (int)ev.getY();
this.mScrolling =
Math.abs(this.mCurrentY - this.mStartY) > (this.mFlingThreshold / 3);
if (this.mFlingingStarted) {
// Don't allow scrolling
this.mScrolling = false;
}
// With flinging support
if (this.mFlingingView != null) {
// Only if event has changed (and only to the right and if not scrolling)
if (!this.mScrolling) {
if (ev.getX() >= this.mStartX && (ev.getX() - this.mCurrentX != 0)) {
this.mCurrentX = (int)ev.getX();
this.mTranslationX = this.mCurrentX - this.mStartX;
this.mFlingingView.setTranslationX(this.mTranslationX);
this.mFlingingView.setPressed(false);
// Started
if (!this.mFlingingStarted) {
// Flinging starting
if (!mMoveStarted) {
if (!this.mOnItemFlingerListener.onItemFlingerStart(
this,
this.mFlingingView,
this.mFlingingViewPos,
this.mFlingingView.getId())) {
this.mCurrentX = 0;
this.mTranslationX = 0;
this.mFlingingView.setTranslationX(this.mTranslationX);
this.mFlingingView.setPressed(false);
break;
}
}
mMoveStarted = true;
if (this.mTranslationX > this.mFlingThreshold) {
this.mFlingingStarted = true;
}
}
// Detect if flinging occurs
float flingLimit =
(this.mFlingingViewWidth * this.mFlingRemovePercentaje);
if (!this.mFlinging && this.mTranslationX > flingLimit) {
// Flinging occurs. Mark and raise an event
this.mFlinging = true;
final ItemFlingerResponder responder =
new ItemFlingerResponder();
responder.mItemView = this.mFlingingView;
// Request a response (we need to do this in background for
// get new events)
this.mFlingingView.post(new Runnable() {
@Override
@SuppressWarnings("synthetic-access")
public void run() {
FlingerListView.
this.mOnItemFlingerListener.onItemFlingerEnd(
responder,
FlingerListView.this,
FlingerListView.this.mFlingingView,
FlingerListView.this.mFlingingViewPos,
FlingerListView.this.mFlingingView.getId());
}
});
}
}
} else {
this.mCurrentX = 0;
this.mTranslationX = 0;
this.mFlingingView.setTranslationX(this.mTranslationX);
this.mFlingingView.setPressed(false);
}
}
if (this.mFlingingStarted) {
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// Clear flinging (only if not waiting confirmation)
// On scrolling, flinging has no effect
if (!this.mFlinging || this.mScrolling) {
this.mStartX = 0;
this.mCurrentX = 0;
this.mTranslationX = 0;
if (this.mFlingingView != null) {
this.mFlingingView.setTranslationX(0);
}
} else {
// Force to display at the limit
if (this.mFlingingView != null) {
float flingLimit =
(this.mFlingingViewWidth * this.mFlingRemovePercentaje);
this.mFlingingView.setTranslationX(flingLimit);
}
}
// What is the motion
if (!this.mScrolling && this.mFlingingView != null) {
if(!this.mMoveStarted && !this.mLongPress) {
this.mFlingingViewPressed = false;
this.mFlingingView.removeCallbacks(this.mLongPressDetection);
this.mFlingingView.setPressed(true);
this.mFlingingView.postDelayed(new Runnable() {
@Override
@SuppressWarnings("synthetic-access")
public void run() {
FlingerListView.this.mFlingingView.setPressed(false);
}
}, PRESSED_DELAY_TIME);
performItemClick(
this.mFlingingView,
this.mFlingingViewPos,
this.mFlingingView.getId());
// Handled
this.mFlingingView.setPressed(false);
}
return true;
}
// Scrolling -> Remove any status (don't handle event)
if (this.mFlingingView != null) {
this.mFlingingView.setPressed(false);
}
break;
default:
break;
}
return super.onTouchEvent(ev);
}
/**
* Method that clean the internal variables
* @hide
*/
void clearVars() {
this.mScrolling = false;
this.mFlinging = false;
this.mLongPress = false;
this.mFlingingStarted = false;
this.mMoveStarted = false;
this.mFlingingView = null;
}
}