I'm using a custom Spinner widget with the code below.
Everything works fine on most devices except on Samsung's devices with Android 5.0. On click the Spinner should display a list of values but that doesn't happen.
On emulators and others brands devices with Android 5.0 it works fine.
Has anyone faced a similiar isse or have any idea of what might be happening?
xml
<?xml version="1.0" encoding="utf-8"?>
<Spinner
android:id="#+id/_combo_spinner"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_weight="1"
android:focusable="false"
android:background="#null"
android:clickable="false"
android:paddingBottom="#dimen/cell_text_section_text_padding_bottom"
android:paddingLeft="#dimen/cell_text_section_text_padding_left"
android:paddingRight="#dimen/cell_text_section_text_padding_right"
android:paddingTop="#dimen/cell_text_section_text_padding_top"
android:spinnerMode="dropdown" />
<View
android:layout_width="#dimen/drawable_stroke_width"
android:layout_height="match_parent"
android:layout_marginBottom="5dp"
android:layout_marginTop="3dp"
android:background="#color/stroke_dark_grey"
android:paddingBottom="#dimen/cell_text_section_text_padding_bottom"
android:paddingTop="#dimen/cell_text_section_text_padding_top" />
<ImageView
style="#style/image__default"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:layout_marginLeft="#dimen/cell_text_section_text_padding_left"
android:layout_marginRight="#dimen/cell_text_section_text_padding_right"
android:src="#drawable/ic_action_expand" />
Java
public class ComboBoxView extends LinearLayout {
private Spinner mSpinner;
private OnItemSelectedListener mListener;
public ComboBoxView(Context context) {
super(context);
initializeLayout(context);
}
public ComboBoxView(Context context, AttributeSet attrs) {
super(context, attrs);
initializeLayout(context);
}
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
public ComboBoxView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initializeLayout(context);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ComboBoxView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initializeLayout(context);
}
// Internal methods:
/**
* Initializes the layout
*
* #param context
*/
private void initializeLayout(final Context context) {
mListener = null;
// Inflate and retrieve the views:
this.setOrientation(LinearLayout.VERTICAL);
LayoutInflater.from(context).inflate(R.layout.view_combo_box, this);
mSpinner = (Spinner) findViewById(R.id._combo_spinner);
// Finish initialization:
final int paddingTop = (int) getResources().getDimension(R.dimen.cell_text_section_text_padding_top);
final int paddingBottom = (int) getResources().getDimension(R.dimen.cell_text_section_text_padding_bottom);
final int paddingLeft = (int) getResources().getDimension(R.dimen.cell_text_section_text_padding_left);
final int paddingRight = (int) getResources().getDimension(R.dimen.cell_text_section_text_padding_right);
setOnClickListener(onClick);
setOrientation(LinearLayout.HORIZONTAL);
setBackgroundResource(R.drawable.button_primary);
setClickable(true);
setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
private final OnClickListener onClick = new OnClickListener() {
#Override
public void onClick(View v) {
mSpinner.performClick();
}
};
#Override
public void clearFocus() {
super.clearFocus();
mSpinner.clearFocus();
}
// External methods:
/**
* Interface definition for a callback to be invoked when
* an item in this view has been selected (extracted from {#link AdapterView.OnItemSelectedListener}).
*/
public interface OnItemSelectedListener {
/**
* <p>Callback method to be invoked when an item in this view has been
* selected. This callback is invoked only when the newly selected
* position is different from the previously selected position or if
* there was no selected item.</p>
* <p/>
* Impelmenters can call getItemAtPosition(position) if they need to access the
* data associated with the selected item.
*
* #param parent The ComboBoxView where the selection happened
* #param position The position of the view in the adapter
* #param id The row id of the item that is selected
*/
void onItemSelected(ComboBoxView parent, int position, long id);
/**
* Callback method to be invoked when the selection disappears from this
* view. The selection can disappear for instance when touch is activated
* or when the adapter becomes empty.
*
* #param parent The ComboBoxView that now contains no selected item.
*/
void onNothingSelected(ComboBoxView parent);
}
public void setValuesAsString(final List<String> newValues) {
setValuesAsString(newValues, 0);
}
public void setValuesAsString(final List<String> newValues, int initialValue) {
List<CharSequence> result = new ArrayList<CharSequence>(newValues.size());
for(String value : newValues) {
result.add(value);
}
setValues(result, initialValue);
}
public void setValues(final List<CharSequence> newValues) {
setValues(newValues, 0);
}
public void setValues(final List<CharSequence> newValues, int initialValue) {
if((initialValue >= newValues.size()) || (initialValue < -1)) {
IllegalArgumentException ex = new IllegalArgumentException("Invalid value for initialValue");
LOG.error(LOG.SOURCE.UI, "Invalid",ex);
throw ex;
}
// Prepare the list of elements:
// NOTE: The last item in ComboBoxArrayAdapter must be empty. Items should also contain the
// same number of lines as the "tallest" entry:
final List<CharSequence> finalValues = new ArrayList<CharSequence>(newValues.size());
finalValues.addAll(newValues);
int maxLines = 1;
for(CharSequence text : newValues) {
final String[] lines = text.toString().split("\r\n|\r|\n");
maxLines = Math.max(maxLines, lines.length);
}
finalValues.add("");
// Prepare spinner:
final ComboBoxArrayAdapter adapter = new ComboBoxArrayAdapter(this.getContext(), R.layout.view_combo_box_item, finalValues);
adapter.setDropDownViewResource(R.layout.view_combo_box_item_dropdown);
adapter.setMaxLines(maxLines);
mSpinner.setOnItemSelectedListener(null);
mSpinner.setAdapter(adapter);
mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
boolean firstSelection = true;
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (mListener != null) {
int index = (position >= (mSpinner.getCount() - 1)) ? -1 : position;
mListener.onItemSelected(ComboBoxView.this, index, id);
}
}
#Override
public void onNothingSelected(AdapterView<?> parent) {
if (mListener != null) {
mListener.onNothingSelected(ComboBoxView.this);
}
}
});
if (mListener != null) {
mListener.onNothingSelected(this);
}
// Set initial selection:
if(initialValue != -1) {
mSpinner.setSelection(initialValue);
} else {
mSpinner.setSelection(newValues.size());
}
}
public void setOnItemSelectedListener(final OnItemSelectedListener listener) {
mListener = listener;
}
public int getSelectedItem() {
int result = mSpinner.getSelectedItemPosition();
if(result >= mSpinner.getCount()) {
result = -1;
}
return result;
}
Spinner
Example Result
Thanks in advance.
I finally fixed this!
The android property clickable was set to false, but the click behaviour was performed in the ComboBoxView.java file on the following code:
private final OnClickListener onClick = new OnClickListener() {
#Override
public void onClick(View v) {
mSpinner.performClick();
}
};
This was working everywhere (devices and emulators) except on Samsung devices with Android 5.0. This I couldn't figure out why.
After I changed the cliclabke property to true it started working.
android:clickable="true"
Thanks.
Related
I want to edit HideBottomViewOnScrollBehavior class so that it can be reversed
to work as HideTopViewOnScrollBehavior
this is the class code :
/** * The {#link Behavior} for a View within a {#link
CoordinatorLayout} to hide the view off the * bottom of the screen
when scrolling down, and show it when scrolling up. */
public class HideBottomViewOnScrollBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
protected static final int ENTER_ANIMATION_DURATION = 225;
protected static final int EXIT_ANIMATION_DURATION = 175;
private static final int STATE_SCROLLED_DOWN = 1;
private static final int STATE_SCROLLED_UP = 2;
private int height = 0;
private int currentState = STATE_SCROLLED_UP;
private int additionalHiddenOffsetY = 0;
#Nullable private ViewPropertyAnimator currentAnimator;
public HideBottomViewOnScrollBehavior() {}
public HideBottomViewOnScrollBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onLayoutChild(
#NonNull CoordinatorLayout parent, #NonNull V child, int layoutDirection) {
ViewGroup.MarginLayoutParams paramsCompat =
(ViewGroup.MarginLayoutParams) child.getLayoutParams();
height = child.getMeasuredHeight() + paramsCompat.bottomMargin;
return super.onLayoutChild(parent, child, layoutDirection);
}
/**
* Sets an additional offset for the y position used to hide the view.
*
* #param child the child view that is hidden by this behavior
* #param offset the additional offset in pixels that should be added when the view slides away
*/
public void setAdditionalHiddenOffsetY(#NonNull V child, #Dimension int offset) {
additionalHiddenOffsetY = offset;
if (currentState == STATE_SCROLLED_DOWN) {
child.setTranslationY(height + additionalHiddenOffsetY);
}
}
#Override
public boolean onStartNestedScroll(
#NonNull CoordinatorLayout coordinatorLayout,
#NonNull V child,
#NonNull View directTargetChild,
#NonNull View target,
int nestedScrollAxes,
int type) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
#Override
public void onNestedScroll(
CoordinatorLayout coordinatorLayout,
#NonNull V child,
#NonNull View target,
int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed,
int type,
#NonNull int[] consumed) {
if (dyConsumed > 0) {
slideDown(child);
} else if (dyConsumed < 0) {
slideUp(child);
}
}
/** * Perform an animation that will slide the child from it's
current position to be totally on the * screen. */
public void slideUp(#NonNull V child) {
if (currentState == STATE_SCROLLED_UP) {
return;
}
if (currentAnimator != null) {
currentAnimator.cancel();
child.clearAnimation();
}
currentState = STATE_SCROLLED_UP;
animateChildTo(
child, 0, ENTER_ANIMATION_DURATION, AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
}
/** * Perform an animation that will slide the child from it's
current position to be totally off the * screen. */
public void slideDown(#NonNull V child) {
if (currentState == STATE_SCROLLED_DOWN) {
return;
}
if (currentAnimator != null) {
currentAnimator.cancel();
child.clearAnimation();
}
currentState = STATE_SCROLLED_DOWN;
animateChildTo(
child,
height + additionalHiddenOffsetY,
EXIT_ANIMATION_DURATION,
AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR);
}
private void animateChildTo(
#NonNull V child, int targetY, long duration, TimeInterpolator interpolator) {
currentAnimator =
child
.animate()
.translationY(targetY)
.setInterpolator(interpolator)
.setDuration(duration)
.setListener(
new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
currentAnimator = null;
}
});
}
}
Can any one give me a source to learn about themes and animations in android?
I have the following layout:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Select an option" />
I use the above layout to set the default text of a spinner button, using this class:
/**
* Decorator Adapter to allow a Spinner to show a 'Nothing Selected...' initially
* displayed instead of the first choice in the Adapter.
*/
public class NothingSelectedSpinnerAdapter implements SpinnerAdapter, ListAdapter {
protected static final int EXTRA = 1;
protected SpinnerAdapter adapter;
protected Context context;
protected int nothingSelectedLayout;
protected int nothingSelectedDropdownLayout;
protected LayoutInflater layoutInflater;
/**
* Use this constructor to have NO 'Select One...' item, instead use
* the standard prompt or nothing at all.
* #param spinnerAdapter wrapped Adapter.
* #param nothingSelectedLayout layout for nothing selected, perhaps
* you want text grayed out like a prompt...
* #param context
*/
public NothingSelectedSpinnerAdapter(
SpinnerAdapter spinnerAdapter,
int nothingSelectedLayout, Context context) {
this(spinnerAdapter, nothingSelectedLayout, -1, context);
}
/**
* Use this constructor to Define your 'Select One...' layout as the first
* row in the returned choices.
* If you do this, you probably don't want a prompt on your spinner or it'll
* have two 'Select' rows.
* #param spinnerAdapter wrapped Adapter. Should probably return false for isEnabled(0)
* #param nothingSelectedLayout layout for nothing selected, perhaps you want
* text grayed out like a prompt...
* #param nothingSelectedDropdownLayout layout for your 'Select an Item...' in
* the dropdown.
* #param context
*/
public NothingSelectedSpinnerAdapter(SpinnerAdapter spinnerAdapter,
int nothingSelectedLayout, int nothingSelectedDropdownLayout, Context context) {
this.adapter = spinnerAdapter;
this.context = context;
this.nothingSelectedLayout = nothingSelectedLayout;
this.nothingSelectedDropdownLayout = nothingSelectedDropdownLayout;
layoutInflater = LayoutInflater.from(context);
}
#Override
public final View getView(int position, View convertView, ViewGroup parent) {
// This provides the View for the Selected Item in the Spinner, not
// the dropdown (unless dropdownView is not set).
if (position == 0) {
return getNothingSelectedView(parent);
}
return adapter.getView(position - EXTRA, null, parent); // Could re-use
// the convertView if possible.
}
/**
* View to show in Spinner with Nothing Selected
* Override this to do something dynamic... e.g. "37 Options Found"
* #param parent
* #return
*/
protected View getNothingSelectedView(ViewGroup parent) {
return layoutInflater.inflate(nothingSelectedLayout, parent, false);
}
#Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
// Android BUG! http://code.google.com/p/android/issues/detail?id=17128 -
// Spinner does not support multiple view types
if (position == 0) {
return nothingSelectedDropdownLayout == -1 ?
new View(context) :
getNothingSelectedDropdownView(parent);
}
// Could re-use the convertView if possible, use setTag...
return adapter.getDropDownView(position - EXTRA, null, parent);
}
/**
* Override this to do something dynamic... For example, "Pick your favorite
* of these 37".
* #param parent
* #return
*/
protected View getNothingSelectedDropdownView(ViewGroup parent) {
return layoutInflater.inflate(nothingSelectedDropdownLayout, parent, false);
}
#Override
public int getCount() {
int count = adapter.getCount();
return count == 0 ? 0 : count + EXTRA;
}
#Override
public Object getItem(int position) {
return position == 0 ? null : adapter.getItem(position - EXTRA);
}
#Override
public int getItemViewType(int position) {
return 0;
}
#Override
public int getViewTypeCount() {
return 1;
}
#Override
public long getItemId(int position) {
return position >= EXTRA ? adapter.getItemId(position - EXTRA) : position - EXTRA;
}
#Override
public boolean hasStableIds() {
return adapter.hasStableIds();
}
#Override
public boolean isEmpty() {
return adapter.isEmpty();
}
#Override
public void registerDataSetObserver(DataSetObserver observer) {
adapter.registerDataSetObserver(observer);
}
#Override
public void unregisterDataSetObserver(DataSetObserver observer) {
adapter.unregisterDataSetObserver(observer);
}
#Override
public boolean areAllItemsEnabled() {
return false;
}
#Override
public boolean isEnabled(int position) {
return position != 0; // Don't allow the 'nothing selected'
// item to be picked.
}
}
I initialize the above class like this:
NothingSelectedSpinnerAdapter myAdapter = new NothingSelectedSpinnerAdapter(spinnerAdapter, R.layout.layout_pasted_above, getContext());
myAdapter.setAdapter(spinnerAdapter);
However, I want to be able to change the text of the above layout programmatically.
How would I achieve this?
-
Apparently StackOverflow needs more words for me to submit this post, but I have no other important details to add, so I'm just adding this text in so I can actually submit this.
Get the id of your widget and set it to a textview variable in your java class
TextView variable= (TextView)findViewById(R.id.label);
then set the text to whatever string you want
variable.setText("insert your text");
You have to get the reference of TextView here after inflating the method.
protected View getNothingSelectedView(ViewGroup parent) {
View nothingSelectedView = layoutInflater.inflate(nothingSelectedLayout,
parent, false);
TextView labelText =(TextView)nothingSelectedView.findViewById(R.id.label);
labelText.setText("Set You Text Here");
return nothingSelectedView;
}
Just create a field, and expose a public method.
protected View getNothingSelectedView(ViewGroup parent) {
View nothingSelectedView = layoutInflater.inflate(nothingSelectedLayout,
parent,
false);
textViewField = (TextView) nothingSelectedView.findViewById(R.id.label);
return nothingSelectedView;
}
// Expose public method
public void changeText(String text) {
textViewField.setText(text);
}
Call it from wherever
adapter.changeText("new text");
I've created a custom spinner, because again and again, I found that I wanted to make sure that the onItemSelectedListener wasn't triggered when I set my Spinner's initial selection or set a new custom adapter. I only want it triggered when a user actually selects an item.
But for some reason (I'm at a complete loss as to why), my custom spinner doesn't respond to touch events. It's as if it's disabled, even though I've debugged and seen that it's perfectly enabled. But for some reason, my little spinner won't open. Can anyone help me understand why?
Here's the xml:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginTop="#dimen/default_margin"
android:orientation="horizontal">
<my.app.custom.view.MySpinner
android:id="#+id/dog_or_cat_toggle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:layout_margin="0dp"
android:textAlignment="center"
android:gravity="center_vertical|center"
android:padding="0dp"
android:entries="#array/dog_or_cat"
android:spinnerMode="dropdown"
android:background="#drawable/top_to_bottom_gray_gradient"/>
...
</LinearLayout>
And my Custom Spinner:
/* A Spinner dispatches an onItemSelected event when the View is initialized, before the user ever makes a selection.
* This class allows listeners for just the initial selection, just user selections, or both. */
public class MySpinner extends Spinner {
private boolean initialized = false;
private OnItemSelectedListener onItemSelectionInitializedListener;
private OnItemSelectedListener onItemSelectedByUserListener;
private OnItemSelectedListener onItemSelectedListener;
public MySpinner(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MySpinner(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MySpinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.initializeMySpinner();
}
public void setOnItemSelectionInitializedListener(OnItemSelectedListener onItemSelectionInitializedListener) {
this.onItemSelectionInitializedListener = onItemSelectionInitializedListener;
}
public void setOnItemSelectedByUserListener(OnItemSelectedListener onItemSelectedByUserListener) {
this.onItemSelectedByUserListener = onItemSelectedByUserListener;
}
#Override
public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) {
this.onItemSelectedListener = onItemSelectedListener;
}
#Override
public void setAdapter(SpinnerAdapter adapter) {
this.initialized = false;
super.setAdapter(adapter);
}
private void initializeMySpinner() {
super.setOnItemSelectedListener(new OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if(!initialized) {
if(onItemSelectionInitializedListener != null) onItemSelectionInitializedListener.onItemSelected(parent, view, position, id);
if(onItemSelectedListener != null) onItemSelectedListener.onItemSelected(parent, view, position, id);
initialized = true;
} else {
if(onItemSelectedListener != null) onItemSelectedListener.onItemSelected(parent, view, position, id);
if(onItemSelectedByUserListener != null) onItemSelectedByUserListener.onItemSelected(parent, view, position, id);
}
}
#Override
public void onNothingSelected(AdapterView<?> parent) {
if(!initialized) {
if(onItemSelectionInitializedListener != null) onItemSelectionInitializedListener.onNothingSelected(parent);
if(onItemSelectedListener != null) onItemSelectedListener.onNothingSelected(parent);
initialized = true;
} else {
if(onItemSelectedListener != null) onItemSelectedListener.onNothingSelected(parent);
if(onItemSelectedByUserListener != null) onItemSelectedByUserListener.onNothingSelected(parent);
}
}
});
}
}
Don't call one constructor from another. Instead, call super() constructor from each one.
I have faced the same issue some time back and this trick worked, but I'm not sure about the reason.
How can I make my SwipeableCardViews more like the IOS 7 mail app (swipe to show buttons)
So far I have created an android application which allows the user to swipe Cardviews either to the left or to the right. Each card has 2 buttons, which at a later date I will assign functions to.
(As the image below shows):
What I would like to accomplish is (but don't yet know how), is instead of swiping to the left or right to completely remove a card.
Instead I would like either swipe gesture, to reveal a button which was hidden underneath the right or left card.
The IOS 7 mail app already performs this function (screenshot below) I would like to perform this in Android using Cardviews (not ListViews).
How can I go about doing this?
My code so far:
MainActivity.java
public class MainActivity extends ActionBarActivity {
private RecyclerView mRecyclerView;
private CardViewAdapter mAdapter;
private ArrayList<String> mItems;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mItems = new ArrayList<>(30);
for (int i = 0; i < 30; i++) {
mItems.add(String.format("Card number %2d", i));
}
OnItemTouchListener itemTouchListener = new OnItemTouchListener() {
#Override
public void onCardViewTap(View view, int position) {
Toast.makeText(MainActivity.this, "Tapped " + mItems.get(position), Toast.LENGTH_SHORT).show();
}
#Override
public void onButton1Click(View view, int position) {
Toast.makeText(MainActivity.this, "Clicked Button1 in " + mItems.get(position), Toast.LENGTH_SHORT).show();
}
#Override
public void onButton2Click(View view, int position) {
Toast.makeText(MainActivity.this, "Clicked Button2 in " + mItems.get(position), Toast.LENGTH_SHORT).show();
}
};
mAdapter = new CardViewAdapter(mItems, itemTouchListener);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(mAdapter);
SwipeableRecyclerViewTouchListener swipeTouchListener =
new SwipeableRecyclerViewTouchListener(mRecyclerView,
new SwipeableRecyclerViewTouchListener.SwipeListener() {
#Override
public boolean canSwipe(int position) {
return true;
}
#Override
public void onDismissedBySwipeLeft(RecyclerView recyclerView, int[] reverseSortedPositions) {
for (int position : reverseSortedPositions) {
mItems.remove(position);
mAdapter.notifyItemRemoved(position);
}
mAdapter.notifyDataSetChanged();
}
#Override
public void onDismissedBySwipeRight(RecyclerView recyclerView, int[] reverseSortedPositions) {
for (int position : reverseSortedPositions) {
mItems.remove(position);
mAdapter.notifyItemRemoved(position);
}
mAdapter.notifyDataSetChanged();
}
});
mRecyclerView.addOnItemTouchListener(swipeTouchListener);
}
/**
* Interface for the touch events in each item
*/
public interface OnItemTouchListener {
/**
* Callback invoked when the user Taps one of the RecyclerView items
*
* #param view the CardView touched
* #param position the index of the item touched in the RecyclerView
*/
public void onCardViewTap(View view, int position);
/**
* Callback invoked when the Button1 of an item is touched
*
* #param view the Button touched
* #param position the index of the item touched in the RecyclerView
*/
public void onButton1Click(View view, int position);
/**
* Callback invoked when the Button2 of an item is touched
*
* #param view the Button touched
* #param position the index of the item touched in the RecyclerView
*/
public void onButton2Click(View view, int position);
}
/**
* A simple adapter that loads a CardView layout with one TextView and two Buttons, and
* listens to clicks on the Buttons or on the CardView
*/
public class CardViewAdapter extends RecyclerView.Adapter<CardViewAdapter.ViewHolder> {
private List<String> cards;
private OnItemTouchListener onItemTouchListener;
public CardViewAdapter(List<String> cards, OnItemTouchListener onItemTouchListener) {
this.cards = cards;
this.onItemTouchListener = onItemTouchListener;
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.card_view_layout, viewGroup, false);
return new ViewHolder(v);
}
#Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
viewHolder.title.setText(cards.get(i));
}
#Override
public int getItemCount() {
return cards == null ? 0 : cards.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
private TextView title;
private Button button1;
private Button button2;
public ViewHolder(View itemView) {
super(itemView);
title = (TextView) itemView.findViewById(R.id.card_view_title);
button1 = (Button) itemView.findViewById(R.id.card_view_button1);
button2 = (Button) itemView.findViewById(R.id.card_view_button2);
button1.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
onItemTouchListener.onButton1Click(v, getPosition());
}
});
button2.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
onItemTouchListener.onButton2Click(v, getPosition());
}
});
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
onItemTouchListener.onCardViewTap(v, getPosition());
}
});
}
}
}
}
SwipeableRecyclerViewTouchListener.java
public class SwipeableRecyclerViewTouchListener implements RecyclerView.OnItemTouchListener {
// Cached ViewConfiguration and system-wide constant values
private int mSlop;
private int mMinFlingVelocity;
private int mMaxFlingVelocity;
private long mAnimationTime;
// Fixed properties
private RecyclerView mRecyclerView;
private SwipeListener mSwipeListener;
private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero
// Transient properties
private List<PendingDismissData> mPendingDismisses = new ArrayList<>();
private int mDismissAnimationRefCount = 0;
private float mDownX;
private float mDownY;
private boolean mSwiping;
private int mSwipingSlop;
private VelocityTracker mVelocityTracker;
private int mDownPosition;
private View mDownView;
private boolean mPaused;
private float mFinalDelta;
/**
* Constructs a new swipe touch listener for the given {#link android.support.v7.widget.RecyclerView}
*
* #param recyclerView The recycler view whose items should be dismissable by swiping.
* #param listener The listener for the swipe events.
*/
public SwipeableRecyclerViewTouchListener(RecyclerView recyclerView, SwipeListener listener) {
ViewConfiguration vc = ViewConfiguration.get(recyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 16;
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
mAnimationTime = recyclerView.getContext().getResources().getInteger(
android.R.integer.config_shortAnimTime);
mRecyclerView = recyclerView;
mSwipeListener = listener;
/**
* This will ensure that this SwipeableRecyclerViewTouchListener is paused during list view scrolling.
* If a scroll listener is already assigned, the caller should still pass scroll changes through
* to this listener.
*/
mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
setEnabled(newState != RecyclerView.SCROLL_STATE_DRAGGING);
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
}
});
}
/**
* Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures.
*
* #param enabled Whether or not to watch for gestures.
*/
public void setEnabled(boolean enabled) {
mPaused = !enabled;
}
#Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent motionEvent) {
return handleTouchEvent(motionEvent);
}
#Override
public void onTouchEvent(RecyclerView rv, MotionEvent motionEvent) {
handleTouchEvent(motionEvent);
}
private boolean handleTouchEvent(MotionEvent motionEvent) {
if (mViewWidth < 2) {
mViewWidth = mRecyclerView.getWidth();
}
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
if (mPaused) {
break;
}
// Find the child view that was touched (perform a hit test)
Rect rect = new Rect();
int childCount = mRecyclerView.getChildCount();
int[] listViewCoords = new int[2];
mRecyclerView.getLocationOnScreen(listViewCoords);
int x = (int) motionEvent.getRawX() - listViewCoords[0];
int y = (int) motionEvent.getRawY() - listViewCoords[1];
View child;
for (int i = 0; i < childCount; i++) {
child = mRecyclerView.getChildAt(i);
child.getHitRect(rect);
if (rect.contains(x, y)) {
mDownView = child;
break;
}
}
if (mDownView != null) {
mDownX = motionEvent.getRawX();
mDownY = motionEvent.getRawY();
mDownPosition = mRecyclerView.getChildPosition(mDownView);
if (mSwipeListener.canSwipe(mDownPosition)) {
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(motionEvent);
} else {
mDownView = null;
}
}
break;
}
case MotionEvent.ACTION_CANCEL: {
if (mVelocityTracker == null) {
break;
}
if (mDownView != null && mSwiping) {
// cancel
mDownView.animate()
.translationX(0)
.alpha(1)
.setDuration(mAnimationTime)
.setListener(null);
}
mVelocityTracker.recycle();
mVelocityTracker = null;
mDownX = 0;
mDownY = 0;
mDownView = null;
mDownPosition = ListView.INVALID_POSITION;
mSwiping = false;
break;
}
case MotionEvent.ACTION_UP: {
if (mVelocityTracker == null) {
break;
}
mFinalDelta = motionEvent.getRawX() - mDownX;
mVelocityTracker.addMovement(motionEvent);
mVelocityTracker.computeCurrentVelocity(1000);
float velocityX = mVelocityTracker.getXVelocity();
float absVelocityX = Math.abs(velocityX);
float absVelocityY = Math.abs(mVelocityTracker.getYVelocity());
boolean dismiss = false;
boolean dismissRight = false;
if (Math.abs(mFinalDelta) > mViewWidth / 2 && mSwiping) {
dismiss = true;
dismissRight = mFinalDelta > 0;
} else if (mMinFlingVelocity <= absVelocityX && absVelocityX <= mMaxFlingVelocity
&& absVelocityY < absVelocityX && mSwiping) {
// dismiss only if flinging in the same direction as dragging
dismiss = (velocityX < 0) == (mFinalDelta < 0);
dismissRight = mVelocityTracker.getXVelocity() > 0;
}
if (dismiss && mDownPosition != ListView.INVALID_POSITION) {
// dismiss
final View downView = mDownView; // mDownView gets null'd before animation ends
final int downPosition = mDownPosition;
++mDismissAnimationRefCount;
mDownView.animate()
.translationX(dismissRight ? mViewWidth : -mViewWidth)
.alpha(0)
.setDuration(mAnimationTime)
.setListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
performDismiss(downView, downPosition);
}
});
} else {
// cancel
mDownView.animate()
.translationX(0)
.alpha(1)
.setDuration(mAnimationTime)
.setListener(null);
}
mVelocityTracker.recycle();
mVelocityTracker = null;
mDownX = 0;
mDownY = 0;
mDownView = null;
mDownPosition = ListView.INVALID_POSITION;
mSwiping = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (mVelocityTracker == null || mPaused) {
break;
}
mVelocityTracker.addMovement(motionEvent);
float deltaX = motionEvent.getRawX() - mDownX;
float deltaY = motionEvent.getRawY() - mDownY;
if (!mSwiping && Math.abs(deltaX) > mSlop && Math.abs(deltaY) < Math.abs(deltaX) / 2) {
mSwiping = true;
mSwipingSlop = (deltaX > 0 ? mSlop : -mSlop);
}
if (mSwiping) {
mDownView.setTranslationX(deltaX - mSwipingSlop);
mDownView.setAlpha(Math.max(0f, Math.min(1f,
1f - Math.abs(deltaX) / mViewWidth)));
return true;
}
break;
}
}
return false;
}
private void performDismiss(final View dismissView, final int dismissPosition) {
// Animate the dismissed list item to zero-height and fire the dismiss callback when
// all dismissed list item animations have completed. This triggers layout on each animation
// frame; in the future we may want to do something smarter and more performant.
final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
final int originalHeight = dismissView.getHeight();
ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(mAnimationTime);
animator.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
--mDismissAnimationRefCount;
if (mDismissAnimationRefCount == 0) {
// No active animations, process all pending dismisses.
// Sort by descending position
Collections.sort(mPendingDismisses);
int[] dismissPositions = new int[mPendingDismisses.size()];
for (int i = mPendingDismisses.size() - 1; i >= 0; i--) {
dismissPositions[i] = mPendingDismisses.get(i).position;
}
if (mFinalDelta > 0) {
mSwipeListener.onDismissedBySwipeRight(mRecyclerView, dismissPositions);
} else {
mSwipeListener.onDismissedBySwipeLeft(mRecyclerView, dismissPositions);
}
// Reset mDownPosition to avoid MotionEvent.ACTION_UP trying to start a dismiss
// animation with a stale position
mDownPosition = ListView.INVALID_POSITION;
ViewGroup.LayoutParams lp;
for (PendingDismissData pendingDismiss : mPendingDismisses) {
// Reset view presentation
pendingDismiss.view.setAlpha(1f);
pendingDismiss.view.setTranslationX(0);
lp = pendingDismiss.view.getLayoutParams();
lp.height = originalHeight;
pendingDismiss.view.setLayoutParams(lp);
}
// Send a cancel event
long time = SystemClock.uptimeMillis();
MotionEvent cancelEvent = MotionEvent.obtain(time, time,
MotionEvent.ACTION_CANCEL, 0, 0, 0);
mRecyclerView.dispatchTouchEvent(cancelEvent);
mPendingDismisses.clear();
}
}
});
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
lp.height = (Integer) valueAnimator.getAnimatedValue();
dismissView.setLayoutParams(lp);
}
});
mPendingDismisses.add(new PendingDismissData(dismissPosition, dismissView));
animator.start();
}
/**
* The callback interface used by {#link SwipeableRecyclerViewTouchListener} to inform its client
* about a swipe of one or more list item positions.
*/
public interface SwipeListener {
/**
* Called to determine whether the given position can be swiped.
*/
boolean canSwipe(int position);
/**
* Called when the item has been dismissed by swiping to the left.
*
* #param recyclerView The originating {#link android.support.v7.widget.RecyclerView}.
* #param reverseSortedPositions An array of positions to dismiss, sorted in descending
* order for convenience.
*/
void onDismissedBySwipeLeft(RecyclerView recyclerView, int[] reverseSortedPositions);
/**
* Called when the item has been dismissed by swiping to the right.
*
* #param recyclerView The originating {#link android.support.v7.widget.RecyclerView}.
* #param reverseSortedPositions An array of positions to dismiss, sorted in descending
* order for convenience.
*/
void onDismissedBySwipeRight(RecyclerView recyclerView, int[] reverseSortedPositions);
}
class PendingDismissData implements Comparable<PendingDismissData> {
public int position;
public View view;
public PendingDismissData(int position, View view) {
this.position = position;
this.view = view;
}
#Override
public int compareTo(#NonNull PendingDismissData other) {
// Sort by descending position
return other.position - position;
}
}
}
activity_main.xml
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical">
</android.support.v7.widget.RecyclerView>
card_view_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="5dp"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground"
android:orientation="vertical"
card_view:cardCornerRadius="5dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/card_view_title"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textColor="#android:color/black"
android:textSize="24sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_below="#id/card_view_title"
android:layout_centerHorizontal="true"
android:gravity="center">
<Button
android:id="#+id/card_view_button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button1" />
<Button
android:id="#+id/card_view_button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button2" />
</LinearLayout>
</RelativeLayout>
</android.support.v7.widget.CardView>
Just for a reference you can use these swipe layout library . its includes swiping actions which you were searching . Hope it ill be helpful
I've just done this. This is how it looks like:
I don't have any CardView but you can have any layout (so you can add the support lib CardView or whatever you like).
This is how I did it. I used this library. Since the library has minSdkVersion 15 and we have 14, I copy-pasted the following classes directly from GitHub: OnItemClickListener, RecyclerViewAdapter, SwipeableItemClickListener, SwipeToDismissTouchListener and ViewAdapter (this are the only ones you need if you use a RecyclerView).
Because the lib has not been updated yet, you'll need to add the following method into SwipeableItemClickListener:
#Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
Your list item layout should have the following structure:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="#dimen/list_item_article_height"
>
<!-- This is the layout you see always -->
<include
layout="#layout/list_item_article"
tools:visibility="gone"
/>
<!-- This is the layout shown after swiping. Must have visibility="gone" -->
<LinearLayout
android:visibility="gone"
tools:visibility="visible"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
>
<TextView
android:id="#+id/swipe_cancel"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="2"
android:text="#string/swipe_cancel"
android:gravity="center"
/>
<TextView
android:id="#+id/swipe_delete"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="#string/swipe_delete"
android:gravity="center"
android:background="#f00"
android:textColor="#fff"
/>
</LinearLayout>
</FrameLayout>
Then simply add this code to your Activity/Fragment after creating the RecyclerView and setting it's adapter:
final SwipeToDismissTouchListener<RecyclerViewAdapter> touchListener =
new SwipeToDismissTouchListener<>(
new RecyclerViewAdapter(mRecyclerView),
new SwipeToDismissTouchListener.DismissCallbacks<RecyclerViewAdapter>() {
#Override
public boolean canDismiss(int position) {
return true;
}
#Override
public void onDismiss(RecyclerViewAdapter recyclerView, int position) {
// remove item at position and notify adapter
}
}
);
mRecyclerView.setOnTouchListener(touchListener);
mRecyclerView.addOnScrollListener((RecyclerView.OnScrollListener) touchListener.makeScrollListener());
mRecyclerView.addOnItemTouchListener(new SwipeableItemClickListener(
getActivity(),
new OnItemClickListener() {
#Override
public void onItemClick(View view, int position) {
if (view.getId() == R.id.swipe_delete) {
touchListener.processPendingDismisses();
} else {
touchListener.undoPendingDismiss();
}
}
}
));
If you want to have only add and delete button you can use RecyclerView but with RecyclerView.Adapter not CardViewAdapter.
Here is an example:
public class MyActivity extends Activity {
private RecyclerView mRecyclerView;
private RecyclerView.Adapter mAdapter;
private RecyclerView.LayoutManager mLayoutManager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_activity);
mRecyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);
// use this setting to improve performance if you know that changes
// in content do not change the layout size of the RecyclerView
mRecyclerView.setHasFixedSize(true);
// use a linear layout manager
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLayoutManager);
// specify an adapter (see also next example)
mAdapter = new MyAdapter(myDataset);
mRecyclerView.setAdapter(mAdapter);
}
...
}
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private String[] mDataset;
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder
public static class ViewHolder extends RecyclerView.ViewHolder {
// each data item is just a string in this case
public TextView mTextView;
public ViewHolder(TextView v) {
super(v);
mTextView = v;
}
}
// Provide a suitable constructor (depends on the kind of dataset)
public MyAdapter(String[] myDataset) {
mDataset = myDataset;
}
// Create new views (invoked by the layout manager)
#Override
public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
// create a new view
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.my_text_view, parent, false);
// set the view's size, margins, paddings and layout parameters
...
ViewHolder vh = new ViewHolder(v);
return vh;
}
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.mTextView.setText(mDataset[position]);
}
// Return the size of your dataset (invoked by the layout manager)
#Override
public int getItemCount() {
return mDataset.length;
}
}
I'm developing an Android 3.1 application.
I have created my custom ArrayAdapter with an ArrayList. Form is a custom class with two fields: name and FormId.
Here is my ArrayAdapter code:
public class FormAdapter extends ArrayAdapter<Form>
{
private Context context;
private int layoutResourceId;
private List<Form> forms;
private ArrayList<Integer> checkedItemsPosition;
private Button downloadButton;
public ArrayList<Integer> getCheckedItemsPosition()
{
return checkedItemsPosition;
}
public String[] getSelectedFormsId()
{
String[] ids = new String[checkedItemsPosition.size()];
int i = 0;
for(Integer pos : checkedItemsPosition)
{
Form f = forms.get(pos.intValue());
ids[i] = f.FormId;
i++;
}
return ids;
}
/**
* Called when selected forms has been downloaded and save it locally correctly.
*/
public void updateFormsNotDownloaded()
{
for (Integer pos : checkedItemsPosition)
{
remove(forms.get(pos.intValue()));
}
checkedItemsPosition.clear();
notifyDataSetChanged();
}
public FormAdapter(Context context, int textViewResourceId,
List<Form> objects, Button downloadButton)
{
super(context, textViewResourceId, objects);
this.context = context;
this.layoutResourceId = textViewResourceId;
this.forms = objects;
this.checkedItemsPosition = new ArrayList<Integer>();
this.downloadButton = downloadButton;
}
#Override
public int getCount()
{
return forms.size();
}
#Override
public View getView(final int position, View convertView, ViewGroup parent)
{
Log.v("FormAdapter", "getView.postion: " + position);
View row = convertView;
if (row == null)
{
LayoutInflater inflater = ((Activity)context).getLayoutInflater();
row = inflater.inflate(layoutResourceId, parent, false);
}
Form f = forms.get(position);
if (f != null)
{
CheckBox checkBox = (CheckBox)row.findViewById(R.id.itemCheckBox);
if (checkBox != null)
{
checkBox.setText(f.Name);
checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener()
{
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked)
{
//Form f = forms.get(position);
if (isChecked)
{
//checkedItems.add(f.FormId);
checkedItemsPosition.add(new Integer(position));
}
else
{
//checkedItems.remove(checkedItems.indexOf(f.FormId));
checkedItemsPosition.remove(checkedItemsPosition.indexOf(new Integer(position)));
}
downloadButton.setEnabled(checkedItemsPosition.size() > 0);
}
});
}
}
return row;
}
}
List items are custom items with a checkbox. On checkedItemsPosition I store checked items position.
My problem is on updateFormsNotDownloaded method. Why am I getting an UnsupportedOperationException?
I can only think of one reason. The List<> implementation you pass into the ArrayAdapters constructor does not support remove(). You can use an ArrayList to fix that.
If for some reason you are using Arrays.asList() to construct your list from an array you are getting a list which cannot be modified.
The size of the
* {#code List} cannot be modified, i.e. adding and removing are unsupported