blob: 795285b412a3c4756a0b71b6a5bbcaa739af9a1c [file] [log] [blame]
// Copyright 2018 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.widget.selection;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.View.OnTouchListener;
import android.widget.Checkable;
import android.widget.FrameLayout;
import org.chromium.chrome.browser.widget.selection.SelectionDelegate.SelectionObserver;
import java.util.List;
/**
* Provides a generic base class for representing an item that can be selected. When selected, the
* view will be updated to indicate that it is selected. The exact UI changes for selection state
* should be provided by the implementing class.
*
* A selection is initially established via long-press. If a selection is already established,
* clicking on the item will toggle its selection.
*
* @param <E> The type of the item associated with this SelectableItemViewBase.
*/
public abstract class SelectableItemViewBase<E>
extends FrameLayout implements Checkable, OnClickListener, OnLongClickListener,
OnTouchListener, SelectionObserver<E> {
// Heuristic value used to rule out long clicks preceded by long horizontal move. A long click
// is ignored if finger was moved horizontally more than this threshold.
private static final float LONG_CLICK_SLIDE_THRESHOLD_PX = 100.f;
private SelectionDelegate<E> mSelectionDelegate;
private E mItem;
private boolean mIsChecked;
// Controls whether selection should happen during onLongClick.
private boolean mSelectOnLongClick = true;
// X position of touch events to detect the amount of horizontal movement between touch down
// and the position where long click is triggered.
private float mAnchorX;
private float mCurrentX;
/**
* Constructor for inflating from XML.
*/
public SelectableItemViewBase(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Destroys and cleans up itself.
*/
public void destroy() {
if (mSelectionDelegate != null) {
mSelectionDelegate.removeObserver(this);
}
}
/**
* Sets the SelectionDelegate and registers this object as an observer. The SelectionDelegate
* must be set before the item can respond to click events.
* @param delegate The SelectionDelegate that will inform this item of selection changes.
*/
public void setSelectionDelegate(SelectionDelegate<E> delegate) {
if (mSelectionDelegate != delegate) {
if (mSelectionDelegate != null) mSelectionDelegate.removeObserver(this);
mSelectionDelegate = delegate;
mSelectionDelegate.addObserver(this);
}
}
/**
* Controls whether selection happens during onLongClick or onClick.
* @param selectOnLongClick True if selection should happen on longClick, false if selection
* should happen on click instead.
*/
public void setSelectionOnLongClick(boolean selectOnLongClick) {
mSelectOnLongClick = selectOnLongClick;
}
/**
* @param item The item associated with this SelectableItemViewBase.
*/
public void setItem(E item) {
mItem = item;
setChecked(mSelectionDelegate.isItemSelected(item));
}
/**
* @return The item associated with this SelectableItemViewBase.
*/
public E getItem() {
return mItem;
}
// FrameLayout implementations.
@Override
protected void onFinishInflate() {
super.onFinishInflate();
setOnTouchListener(this);
setOnClickListener(this);
setOnLongClickListener(this);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mSelectionDelegate != null) {
setChecked(mSelectionDelegate.isItemSelected(mItem));
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
setChecked(false);
}
// OnTouchListener implementation.
@Override
public final boolean onTouch(View view, MotionEvent event) {
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
// mCurrentX needs init here as well, since we might not get ACTION_MOVE
// for a simple click turning into a long click when selection mode is on.
mAnchorX = mCurrentX = event.getX();
} else if (action == MotionEvent.ACTION_MOVE) {
mCurrentX = event.getX();
}
return false;
}
// OnClickListener implementation.
@Override
public final void onClick(View view) {
assert view == this;
if (!mSelectOnLongClick) {
handleSelection();
return;
}
if (isSelectionModeActive()) {
onLongClick(view);
} else {
onClick();
}
}
// OnLongClickListener implementation.
@Override
public boolean onLongClick(View view) {
assert view == this;
if (Math.abs(mCurrentX - mAnchorX) < LONG_CLICK_SLIDE_THRESHOLD_PX) handleSelection();
return true;
}
private void handleSelection() {
boolean checked = toggleSelectionForItem(mItem);
setChecked(checked);
}
/**
* @return Whether we are currently in selection mode.
*/
protected boolean isSelectionModeActive() {
return mSelectionDelegate.isSelectionEnabled();
}
/**
* Toggles the selection state for a given item.
* @param item The given item.
* @return Whether the item was in selected state after the toggle.
*/
protected boolean toggleSelectionForItem(E item) {
return mSelectionDelegate.toggleSelectionForItem(item);
}
// Checkable implementations.
@Override
public boolean isChecked() {
return mIsChecked;
}
@Override
public void toggle() {
setChecked(!isChecked());
}
@Override
public void setChecked(boolean checked) {
if (checked == mIsChecked) return;
mIsChecked = checked;
updateView();
}
// SelectionObserver implementation.
@Override
public void onSelectionStateChange(List<E> selectedItems) {
setChecked(mSelectionDelegate.isItemSelected(mItem));
}
/**
* Update the view based on whether this item is selected.
*/
protected void updateView() {}
/**
* Same as {@link OnClickListener#onClick(View)} on this.
* Subclasses should override this instead of setting their own OnClickListener because this
* class handles onClick events in selection mode, and won't forward events to subclasses in
* that case.
*/
protected abstract void onClick();
}