I am trying to retrieve the Child Row of the listview when the event swipe take place.I have created a BaseAdapter which sets a customView following is the GetView Method of BaseAdapter:
public View getView(int position, View convertView, ViewGroup parent) {
ModelView modelView = null;
ModelClass model=modelList.get(position);
if(convertView==null)
{
modelView=new ModelView(context,model);
}
else{
Log.d("convertView","NotNUll");
modelView=(modelView) convertView;
}
modelView.setModel(diet);
return modelView;
}
My ModelView Class Looks Like this:
public class ModelView extends LinearLayout {
Context context;
TextView text1,text2,text3;
ImageView img1;
Model d;
public ModelView(Context context,Model d) {
super(context);
this.context=context;
this.d=d;
this.setTag(d);
HookUp();
}
public void HookUp() {
this.setLayoutParams(new ListView.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.MATCH_PARENT));
this.setOrientation(LinearLayout.VERTICAL);
LayoutInflater inflater = LayoutInflater.from(context);
view = inflater.inflate(R.layout.lay, null);
this.addView(view);
textDietHeadLine=(TextView) view.findViewById(R);
text=(TextView) view.findViewById(R.id.txt1);
text=(TextView) view.findViewById(R.id.txt2);
}
}
In My Main Activity on TouchListener when i am retrieving the Tag it always give me null,i dont know where i m going wrong...any help would be appreciated ,although i searched alot on this issue but it was of no avail,i am a beginner in android,and struck on this for quiet a while now,plss helppp :(
Following is the touchListener Code:
public boolean onTouch(View v, MotionEvent e)
{
if (gestureDetector.onTouchEvent(e)){
// Log.d("Diet d","v.gettag());
return true;
}else{
return false;
}
It a bit complicated, I hope i can do it right, in case you want to get particular row which user swiping on, it need to extracting a method which carry deep in ListView calls findMotionRow, this method declare as an actually private visibility for extender, so we must make that method available in our own ListView and learn how to invoke it from the ListView's source code :
public class YourListView extends ListView {
...Constructors...
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mMotionPosition = findMotionRow(ev.getY());
break;
}
return super.onInterceptTouchEvent(ev);
}
private int mMotionPosition;
public int getMotionPosition() {
return mMotionPosition;
}
/**
* Find which position is motion on.
* Note : this method copy into public from 4.0 source code.
* #param y Y coordinate of the motion event.
* #return Selected index (starting at 0) of the data item.
*/
private int findMotionRow(float y) {
int childCount = getChildCount();
if (childCount > 0) {
if (!isStackFromBottom()) {
for (int i = 0; i < childCount; i++) {
View v = getChildAt(i);
if (y <= v.getBottom()) {
return getFirstVisiblePosition() + i;
}
}
} else {
for (int i = childCount - 1; i >= 0; i--) {
View v = getChildAt(i);
if (y >= v.getTop()) {
return getFirstVisiblePosition() + i;
}
}
}
}
return INVALID_POSITION;
}
}
In the proper place, i.e Activity.onCreate(), we can listening the onTouch event and able to use mMotionPosition to take the row's View :
mYourListView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
int hoverChildIndex =
mYourListView.getMotionPosition() - mYourListView.getFirstVisiblePosition();
ModelView hoveringView = (ModelView) mYourListView.getChildAt(hoverChildIndex);
return false;
}
});
Related
I have been working on making Sudoku grid whose cell's value changes whenever I touch on a cell. So I have implemented this sudoku grid in a LinearLayout by Child View, and tried using OnTouch method, but it is not working. I tried using log method to check whether onTouch is actually called, but it seemes that this method is perfectly ignored. I have been searching for solutions on other question, but it seems none of those solutions helped. I feel kinda suck here, and any help would be greatly appreciated.
Here is my code:
SudokuActivity.java
package snacker.nonogramsolver;
import ...; /*many things are imported here*/
public class SudokuActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sudoku);
Button btn = (Button)findViewById(R.id.btn_clear);
Sudoku sdk = new Sudoku(this);
sdk.setOnTouchListener(sdk);
}
}
;
Sudoku.java
package snacker.nonogramsolver;
import ...;
public class Sudoku extends View implements View.OnTouchListener {
int mWidth = 9;
int mHeight = 9;
int mCellWidth, mCellHeight;
int mCellMargin;
int mEdgeThick;
int mStatus;
int mTextSize;
int mXNow = -1, mYNow = -1;
int[][] mBoard = new int[9][9];
Point mBoardPt;
Paint mTextPaint, mTileEdgePaint;
final static int VALID = 0;
public Sudoku(Context context){
super(context);
initializeBoard();
}
public Sudoku(Context context, AttributeSet attrs){
super(context, attrs);
initializeBoard();
}
#Override
protected void onDraw(Canvas canvas){
/* There are some codes here */
Log.d("LogTest","OnDraw Complete");
}
public void initializeBoard(){
for (int x=0; x< mWidth; x++){
for (int y=0; y< mHeight; y++){
mBoard[x][y] = 0;
}
}
invalidate();
}
public boolean onTouch(View v, MotionEvent event){
Log.d("LogTest","Touched?"); /* LOG NOT ACTIVE HERE */
if(event.getAction() == MotionEvent.ACTION_DOWN){
mXNow = getBoardX(event.getX());
Log.d("LogTest","" + mXNow); /* LOG NOT ACTIVE HERE */
mYNow = getBoardY(event.getY());
Log.d("LogTest","" + mYNow); /* LOG NOT ACTIVE HERE */
mBoard[mXNow][mYNow] = mBoard[mXNow][mYNow] + 1;
invalidate();
return true;
}
else return false;
}
int getBoardX(float scrx){
int x = (int)((scrx) / mCellWidth);
if (x < 0) x = 0;
if (x > 8) x= 8;
return x;
}
int getBoardY(float scry){
int y = (int)((scry) / mCellHeight);
if (y < 0) y = 0;
if (y > 8) y = 8;
return y;
}
}
Edit: added activity XML file.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:id="#+id/activity_sudoku"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context="snacker.nonogramsolver.SudokuActivity">
<snacker.nonogramsolver.Sudoku
android:id="#+id/SudokuGrid"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="#+id/btn_clear"
android:layout_width="150dp"
android:layout_height="30dp"
android:layout_weight="0.06"
android:text="Clear" />
</LinearLayout>
You cannot directly add touchListener by just creating object of
Sudoku class. You should add view in xml or programatically.
Your Activity
public class MyActivity extends Activity{
#Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
//initializing custom views
MyCustomView1 myCustomView1 = new MyCustomView1(parameterList);
MyCustomView2 myCustomView2 = new MyCustomView2(parameterList);
//adding both custom views to the main activity
mainView.addView(myCustomView1);
mainView.addView(myCustomView1);
//adding custom listener to the custom view 1
myCustomView1.setCustomEventListener(new OnCustomEventListener() {
#Override
public void onEvent() {
//firing an event of custom view 1
Toast.makeText(MainActivity.this, "Touched custom view 1",
Toast.LENGTH_SHORT).show();
}
});
//adding custom listener to the custom view 2
myCustomView2.setCustomEventListener(new OnCustomEventListener() {
#Override
public void onEvent() {
//firing an event of custom view 2
Toast.makeText(MainActivity.this, "Touched custom view 2",
Toast.LENGTH_SHORT).show();
}
});
}
}
Your CustomView 1
public class MyCustomView1 extends LinearLayout{
OnCustomEventListener myCustomEventListener;
public MyCustomView1(ParameterList){
super(ContextFromParameterList);
//Just adding something to the custom view 1 in order to distinguish it on the screen
TextView tv = new TextView(ContextFromParameterList);
tv.setText("Hello world from custom view 1");
addView(tv);
this.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
//delegating one event to another (delegating touch event to custom event)
if (MyCustomView1.this.myCustomEventListener != null)
MyCustomView1.this.myCustomEventListener.onEvent();
return false;
}
}); }
public void setCustomEventListener(OnCustomEventListener
eventListener) {
//setting custom listener from activity
myCustomEventListener = eventListener; } }
Your CustomView2
public class MyCustomView2 extends LinearLayout {
OnCustomEventListener myCustomEventListener;
public MyCustomView2(ParameterList) {
super(ContextFromParameterList);
//Just adding something to the custom view 1 in order to distinguish it on the screen
TextView tv = new TextView(ContextFromParameterList);
tv.setText("Hello world from custom view 2");
addView(tv);
this.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
//delegating one event to another (delegating touch event to custom event)
if (MyCustomView2.this.myCustomEventListener != null)
MyCustomView2.this.myCustomEventListener.onEvent();
return false;
}
});
}
public void setCustomEventListener(OnCustomEventListener eventListener) {
//setting custom listener from activity
myCustomEventListener = eventListener;
}
}
Your listener interface:
public interface OnCustomEventListener{
//interface defines one method. Can be more and methods may have parameters
public void onEvent();
}
I am a New Android Application Developer. I would like to know how to create endless horizontal scroll view. For example, there are three buttons (Button1, Button2 and Button3). When user scroll the view, I still want to display Button1 again after Button3. Could you please provide any sample code or any idea?
Thanks.
You could check if your button view is still visible. First check if button one is visible:
private boolean isViewVisible(View view) {
Rect scrollBounds = new Rect();
mScrollView.getDrawingRect(scrollBounds);
float top = view.getY();
float bottom = top + view.getHeight();
if (scrollBounds.top < top && scrollBounds.bottom > bottom) {
return true;
} else {
return false;
}
}
If it is not visible, then add button one again. Call this method in a scroll listener every time user scrolls, to check if the button is not visible. If the button is not visible, then add it again.
If you want to make it endless itself, try this:
public class Test extends ListActivity implements OnScrollListener {
Aleph0 adapter = new Aleph0();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setListAdapter(adapter);
getListView().setOnScrollListener(this);
}
public void onScroll(AbsListView view,
int firstVisible, int visibleCount, int totalCount) {
boolean loadMore = /* maybe add a padding */
firstVisible + visibleCount >= totalCount;
if(loadMore) {
adapter.count += visibleCount; // or any other amount
adapter.notifyDataSetChanged();
}
}
public void onScrollStateChanged(AbsListView v, int s) { }
class Aleph0 extends BaseAdapter {
int count = 40; /* starting amount */
public int getCount() { return count; }
public Object getItem(int pos) { return pos; }
public long getItemId(int pos) { return pos; }
public View getView(int pos, View v, ViewGroup p) {
TextView view = new TextView(Test.this);
view.setText("entry " + pos);
return view;
}
}
}
And take a look at this:
Android Endless List
android:how to make infinite scrollview with endless scrolling
Thats it. Just see when the view is out of bounds when the user is scrolling.
And when it is out of view, just re add it.
m trying to build android new inbox style listview with swipe left and right as shown in this image , i have tried 47deg swipelistview but its not that stable , is there any other library available?!
Tried so far with 47 deg
public class MainActivity extends Activity {
Listview pullToRefreshListView;
SwipeListView swipelistview;
ItemAdapter adapter;
List<ItemRow> itemData;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
pullToRefreshListView = (ListView) findViewById(R.id.example_swipe_lv_list);
swipelistview = pullToRefreshListView.getRefreshableView();
itemData = new ArrayList<ItemRow>();
adapter = new ItemAdapter(this, R.layout.custom_row, itemData);
swipelistview.setSwipeListViewListener(new BaseSwipeListViewListener() {
#Override
public void onOpened(int position, boolean toRight) {
if (toRight) {
adapter.remove(position);
Toast.makeText(MainActivity.this, "Open to dismiss",
Toast.LENGTH_SHORT).show();
} // swipelistview.dismiss(position);
else {
Toast.makeText(MainActivity.this, "Open to edit",
Toast.LENGTH_SHORT).show();
}
}
#Override
public void onClosed(int position, boolean fromRight) {
}
#Override
public void onListChanged() {
}
#Override
public void onMove(int position, float x) {
}
#Override
public void onStartOpen(int position, int action, boolean right) {
if (right) {
// adapter.onRight();
swipelistview.getChildAt(position).findViewById(R.id.back)
.setBackgroundColor(Color.GREEN);
swipelistview.getChildAt(position)
.findViewById(R.id.imageViewLeft)
.setVisibility(View.VISIBLE);
swipelistview.getChildAt(position)
.findViewById(R.id.imageViewRight)
.setVisibility(View.GONE);
} else {
// adapter.onLeft();
swipelistview.getChildAt(position).findViewById(R.id.back)
.setBackgroundColor(Color.RED);
swipelistview.getChildAt(position)
.findViewById(R.id.imageViewLeft)
.setVisibility(View.GONE);
swipelistview.getChildAt(position)
.findViewById(R.id.imageViewRight)
.setVisibility(View.VISIBLE);
}
}
#Override
public void onStartClose(int position, boolean right) {
Log.d("swipe", String.format("onStartClose %d", position));
}
#Override
public void onClickFrontView(int position) {
Log.d("swipe", String.format("onClickFrontView %d", position));
// swipelistview.openAnimate(position); //when you touch front
// view it will open
}
#Override
public void onClickBackView(int position) {
Log.d("swipe", String.format("onClickBackView %d", position));
// swipelistview.closeAnimate(position);//when you touch back
// view it will close
}
#Override
public void onDismiss(int[] reverseSortedPositions) {
}
});
// These are the swipe listview settings. you can change these
// setting as your requirement
swipelistview.setSwipeMode(SwipeListView.SWIPE_MODE_BOTH); // there are
// five
// swiping
// modes
swipelistview.setSwipeActionRight(SwipeListView.SWIPE_ACTION_REVEAL); // there
// are
// four
// swipe
// actions
swipelistview.setSwipeActionLeft(SwipeListView.SWIPE_ACTION_REVEAL);
swipelistview.setOffsetRight(convertDpToPixel(0f)); // left side
// offset
swipelistview.setOffsetLeft(convertDpToPixel(0f)); // right side
// offset
swipelistview.setAnimationTime(60); // Animation time
swipelistview.setSwipeOpenOnLongPress(false); // enable or disable
// SwipeOpenOnLongPress
swipelistview.setSwipeCloseAllItemsWhenMoveList(true);
swipelistview.setAdapter(adapter);
for (int i = 0; i < 10; i++) {
itemData.add(new ItemRow("Swipe Item" + i, getResources()
.getDrawable(R.drawable.ic_launcher)));
}
adapter.notifyDataSetChanged();
}
public int convertDpToPixel(float dp) {
DisplayMetrics metrics = getResources().getDisplayMetrics();
float px = dp * (metrics.densityDpi / 160f);
return (int) px;
}
}
Adapter class
public class ItemAdapter extends ArrayAdapter<ItemRow> {
List<ItemRow> data;
Context context;
int layoutResID;
public ItemAdapter(Context context, int layoutResourceId, List<ItemRow> data) {
super(context, layoutResourceId, data);
this.data = data;
this.context = context;
this.layoutResID = layoutResourceId;
// TODO Auto-generated constructor stub
}
NewsHolder holder = null;
View row = null;
#Override
public View getView(int position, View convertView, ViewGroup parent) {
row = convertView;
holder = null;
if (row == null) {
LayoutInflater inflater = ((Activity) context).getLayoutInflater();
row = inflater.inflate(layoutResID, parent, false);
holder = new NewsHolder();
holder.itemName = (TextView) row
.findViewById(R.id.example_itemname);
holder.icon = (ImageView) row.findViewById(R.id.example_image);
holder.imageViewRight = (ImageView) row
.findViewById(R.id.imageViewRight);
holder.imageViewLeft = (ImageView) row
.findViewById(R.id.imageViewLeft);
row.setTag(holder);
} else {
holder = (NewsHolder) row.getTag();
}
ItemRow itemdata = data.get(position);
holder.itemName.setText(itemdata.getItemName());
holder.icon.setImageDrawable(itemdata.getIcon());
return row;
}
public void remove(int pos){
data.remove(pos);
}
public void onLeft() {
holder.imageViewLeft.setVisibility(View.VISIBLE);
holder.imageViewRight.setVisibility(View.GONE);
}
public void onRight() {
holder.imageViewRight.setVisibility(View.VISIBLE);
holder.imageViewLeft.setVisibility(View.GONE);
}
static class NewsHolder {
TextView itemName;
ImageView icon;
ImageView imageViewLeft, imageViewRight;
RelativeLayout mRelativeLayout;
}
Instead of using a custom ListView you can simply support "swipe" gesture on list items onTouch, like the following:
private static final int DEFAULT_THRESHOLD = 128;
row.setOnTouchListener(new View.OnTouchListener() {
int initialX = 0;
final float slop = ViewConfiguration.get(context).getScaledTouchSlop();
public boolean onTouch(final View view, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
initialX = (int) event.getX();
view.setPadding(0, 0, 0, 0);
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
int currentX = (int) event.getX();
int offset = currentX - initialX;
if (Math.abs(offset) > slop) {
view.setPadding(offset, 0, 0, 0);
if (offset > DEFAULT_THRESHOLD) {
// TODO :: Do Right to Left action! And do nothing on action_up.
} else if (offset < -DEFAULT_THRESHOLD) {
// TODO :: Do Left to Right action! And do nothing on action_up.
}
}
} else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
// Animate back if no action was performed.
ValueAnimator animator = ValueAnimator.ofInt(view.getPaddingLeft(), 0);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
view.setPadding((Integer) valueAnimator.getAnimatedValue(), 0, 0, 0);
}
});
animator.setDuration(150);
animator.start();
}
};
I also use reverse animation if no action was performed.
This solution is lightweight so you should not experience any lags.
Check out: SwipeActionAdapter
It's a great library that does exactly what you're asking for. It allows Swipe in both directions with an underlying Layout or Color. It's easy to implement and looks nice!
Updated Answer
As I mentioned previously, I took the same approach and it seems to work as expected. I have added 3 layers to a RelativeLayout. Top layer is what you want to show. Second layer is a plain background with delete icon at the left. Third layer is another plain background with share icon at the right. I implemented a swipe detector class which extends View.OnTouchListener.
public class SwipeDetector implements View.OnTouchListener {
private static final int MIN_DISTANCE = 300;
private static final int MIN_LOCK_DISTANCE = 30; // disallow motion intercept
private boolean motionInterceptDisallowed = false;
private float downX, upX;
private ObjectHolder holder;
private int position;
public SwipeDetector(ObjectHolder h, int pos) {
holder = h;
position = pos;
}
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
downX = event.getX();
return true; // allow other events like Click to be processed
}
case MotionEvent.ACTION_MOVE: {
upX = event.getX();
float deltaX = downX - upX;
if (Math.abs(deltaX) > MIN_LOCK_DISTANCE && listView != null && !motionInterceptDisallowed) {
listView.requestDisallowInterceptTouchEvent(true);
motionInterceptDisallowed = true;
}
if (deltaX > 0) {
holder.deleteView.setVisibility(View.GONE);
} else {
// if first swiped left and then swiped right
holder.deleteView.setVisibility(View.VISIBLE);
}
swipe(-(int) deltaX);
return true;
}
case MotionEvent.ACTION_UP:
upX = event.getX();
float deltaX = upX - downX;
if (Math.abs(deltaX) > MIN_DISTANCE) {
// left or right
swipeRemove();
} else {
swipe(0);
}
if (listView != null) {
listView.requestDisallowInterceptTouchEvent(false);
motionInterceptDisallowed = false;
}
holder.deleteView.setVisibility(View.VISIBLE);
return true;
case MotionEvent.ACTION_CANCEL:
holder.deleteView.setVisibility(View.VISIBLE);
return false;
}
return true;
}
private void swipe(int distance) {
View animationView = holder.mainView;
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) animationView.getLayoutParams();
params.rightMargin = -distance;
params.leftMargin = distance;
animationView.setLayoutParams(params);
}
private void swipeRemove() {
remove(getItem(position));
notifyDataSetChanged();
}
}
public static class ObjectHolder {
public LinearLayout mainView;
public RelativeLayout deleteView;
public RelativeLayout shareView;
/* other views here */
}
I have also added requestDisallowInterceptTouchEvent so that ListView (which is parent) doesn't intercept the touch event when there's some amount of vertical scrolling involved.
I have written a blogpost about it which you can find it here. I have also added a Youtube video for demo.
Old Answer
I implemented one of these myself, but it's a bit different. I use just touch instead of swiping. Touch to open, touch to close. Here's youtube demo.
I created custom ArrayAdapter. To set the layout, I created a custom layout like this.
<RelativeLayout>
<RelativeLayout>
<Stuff that you want at the back of your list/>
</RelativeLayout>
<RelativeLayout>
<Stuff that you want at the front of your list/>
</RelativeLayout>
</RelativeLayout>
Using RelativeLayout, I am putting the top view over the bottom view. Both have same sizes. You can use different layouts for inner layouts.
In Custom ArrayAdapter,
#Override
public view getView(int position, View convertView, ViewGroup parent) {
// get holder and entry
// set each element based on entry preferences
holder.topView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (entry.isSwiped()) {
swipeWithAnimationValue(holder.topView, 1);
entry.setSwiped(false);
} else {
closeOtherSwipes(entry); // if you want to keep only one entry open at a time
swipeWithAnimationValue(holder.topView, 0);
entry.setSwiped(true);
}
}
});
}
Normal Animation would not work as it just shifts the view, but it's still there so if you try to click, the click still occurs on the top view. Hence I have used valueAnimator and actually shifted those lists.
public void swipeWithAnimationValue(final View view, final int direction) {
final int width = view.getWidth();
Log.i(TAG, "view width = " + String.valueOf(width));
ValueAnimator animationSwipe;
int duration = 300;
if (direction == 0) {
animationSwipe = ValueAnimator.ofInt(0, view.getWidth() - 200);
} else {
animationSwipe = ValueAnimator.ofInt(view.getWidth() - 200, 0);
}
animationSwipe.setDuration(duration);
AnimatorUpdateListener maringUpdater = new AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) view.getLayoutParams();
params.rightMargin = -(Integer)animation.getAnimatedValue();
params.leftMargin = (Integer)animation.getAnimatedValue();
view.setLayoutParams(params);
}
};
animationSwipe.addUpdateListener(maringUpdater);
animationSwipe.setRepeatCount(0);
animationSwipe.start();
}
I am attempting to animate the ListView items when a scroll takes place. More specifically, I am trying to emulate the scroll animations from the iMessage app on iOS 7. I found a similar example online:
To clarify, I'm trying to achieve the "fluid" movement effect on the items when the user scrolls, not the animation when a new item is added. I've attempted to modify the Views in my BaseAdapter and I've looked into the AbsListView source to see if I could somehow attach an AccelerateInterpolator somewhere that would adjust the draw coordinates sent to the children Views (if that is even how AbsListView is designed). I've been unable to make any progress so far.
Does anybody have any ideas of how to replicate this behaviour?
For the record to help with googling: this is called "UIKit Dynamics" on ios.
How to replicate Messages bouncing bubbles in iOS 7
It is built-in to recent iOS releases. However it's still somewhat hard to use. (2014) This is the post on it everyone copies:widely copied article Surprisingly, UIKit Dynamics is only available on apple's "collection view", not on apple's "table view" so all the iOS debs are having to convert stuff from table view to "collection view"
The library everyone is using as a starting point is BPXLFlowLayout, since that person pretty much cracked copying the feel of the iphone text messages app. In fact, if you were porting it to Android I guess you could use the parameters in there to get the same feel. FYI I noticed in my android fone collection, HTC phones have this effect, on their UI. Hope it helps. Android rocks!
This implementation works quite good. There is some flickering though, probably because of altered indices when the adapter add new views to top or bottom..That could be possibly solved by watching for changes in the tree and shifting the indices on the fly..
public class ElasticListView extends GridView implements AbsListView.OnScrollListener, View.OnTouchListener {
private static int SCROLLING_UP = 1;
private static int SCROLLING_DOWN = 2;
private int mScrollState;
private int mScrollDirection;
private int mTouchedIndex;
private View mTouchedView;
private int mScrollOffset;
private int mStartScrollOffset;
private boolean mAnimate;
private HashMap<View, ViewPropertyAnimator> animatedItems;
public ElasticListView(Context context) {
super(context);
init();
}
public ElasticListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ElasticListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mScrollState = SCROLL_STATE_IDLE;
mScrollDirection = 0;
mStartScrollOffset = -1;
mTouchedIndex = Integer.MAX_VALUE;
mAnimate = true;
animatedItems = new HashMap<>();
this.setOnTouchListener(this);
this.setOnScrollListener(this);
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (mScrollState != scrollState) {
mScrollState = scrollState;
mAnimate = true;
}
if (scrollState == SCROLL_STATE_IDLE) {
mStartScrollOffset = Integer.MAX_VALUE;
mAnimate = true;
startAnimations();
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mScrollState == SCROLL_STATE_TOUCH_SCROLL) {
if (mStartScrollOffset == Integer.MAX_VALUE) {
mTouchedView = getChildAt(mTouchedIndex - getPositionForView(getChildAt(0)));
if (mTouchedView == null) return;
mStartScrollOffset = mTouchedView.getTop();
} else if (mTouchedView == null) return;
mScrollOffset = mTouchedView.getTop() - mStartScrollOffset;
int tmpScrollDirection;
if (mScrollOffset > 0) {
tmpScrollDirection = SCROLLING_UP;
} else {
tmpScrollDirection = SCROLLING_DOWN;
}
if (mScrollDirection != tmpScrollDirection) {
startAnimations();
mScrollDirection = tmpScrollDirection;
}
if (Math.abs(mScrollOffset) > 200) {
mAnimate = false;
startAnimations();
}
Log.d("test", "direction:" + (mScrollDirection == SCROLLING_UP ? "up" : "down") + ", scrollOffset:" + mScrollOffset + ", toucheId:" + mTouchedIndex + ", fvisible:" + firstVisibleItem + ", " +
"visibleItemCount:" + visibleItemCount + ", " +
"totalCount:" + totalItemCount);
int indexOfLastAnimatedItem = mScrollDirection == SCROLLING_DOWN ?
getPositionForView(getChildAt(0)) + getChildCount() :
getPositionForView(getChildAt(0));
//check for bounds
if (indexOfLastAnimatedItem >= getChildCount()) {
indexOfLastAnimatedItem = getChildCount() - 1;
} else if (indexOfLastAnimatedItem < 0) {
indexOfLastAnimatedItem = 0;
}
if (mScrollDirection == SCROLLING_DOWN) {
setAnimationForScrollingDown(mTouchedIndex - getPositionForView(getChildAt(0)), indexOfLastAnimatedItem, firstVisibleItem);
} else {
setAnimationForScrollingUp(mTouchedIndex - getPositionForView(getChildAt(0)), indexOfLastAnimatedItem, firstVisibleItem);
}
if (Math.abs(mScrollOffset) > 200) {
mAnimate = false;
startAnimations();
mTouchedView = null;
mScrollDirection = 0;
mStartScrollOffset = -1;
mTouchedIndex = Integer.MAX_VALUE;
mAnimate = true;
}
}
}
private void startAnimations() {
for (ViewPropertyAnimator animator : animatedItems.values()) {
animator.start();
}
animatedItems.clear();
}
private void setAnimationForScrollingDown(int indexOfTouchedChild, int indexOflastAnimatedChild, int firstVisibleIndex) {
for (int i = indexOfTouchedChild + 1; i <= indexOflastAnimatedChild; i++) {
View v = getChildAt(i);
v.setTranslationY((-1f * mScrollOffset));
if (!animatedItems.containsKey(v)) {
animatedItems.put(v, v.animate().translationY(0).setDuration(300).setStartDelay(50 * i));
}
}
}
private void setAnimationForScrollingUp(int indexOfTouchedChild, int indexOflastAnimatedChild, int firstVisibleIndex) {
for (int i = indexOfTouchedChild - 1; i > 0; i--) {
View v = getChildAt(i);
v.setTranslationY((-1 * mScrollOffset));
if (!animatedItems.containsKey(v)) {
animatedItems.put(v, v.animate().translationY(0).setDuration(300).setStartDelay(50 * (indexOfTouchedChild - i)));
}
}
}
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
Rect rect = new Rect();
int childCount = getChildCount();
int[] listViewCoords = new int[2];
getLocationOnScreen(listViewCoords);
int x = (int)event.getRawX() - listViewCoords[0];
int y = (int)event.getRawY() - listViewCoords[1];
View child;
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
child.getHitRect(rect);
if (rect.contains(x, y)) {
mTouchedIndex = getPositionForView(child);
break;
}
}
return false;
}
return false;
}
}
I've taken just a few minutes to explore this and it looks like it can be done pretty easily with API 12 and above (hopefully I'm not missing something ...). To get the very basic card effect, all it takes is a couple lines of code at the end of getView() in your Adapter right before you return it to the list. Here's the entire Adapter:
public class MyAdapter extends ArrayAdapter<String>{
private int mLastPosition;
public MyAdapter(Context context, ArrayList<String> objects) {
super(context, 0, objects);
}
private class ViewHolder{
public TextView mTextView;
}
#TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
convertView = LayoutInflater.from(getContext()).inflate(R.layout.grid_item, parent, false);
holder.mTextView = (TextView) convertView.findViewById(R.id.checkbox);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.mTextView.setText(getItem(position));
// This tells the view where to start based on the direction of the scroll.
// If the last position to be loaded is <= the current position, we want
// the views to start below their ending point (500f further down).
// Otherwise, we start above the ending point.
float initialTranslation = (mLastPosition <= position ? 500f : -500f);
convertView.setTranslationY(initialTranslation);
convertView.animate()
.setInterpolator(new DecelerateInterpolator(1.0f))
.translationY(0f)
.setDuration(300l)
.setListener(null);
// Keep track of the last position we loaded
mLastPosition = position;
return convertView;
}
}
Note that I'm keeping track of the last position to be loaded (mLastPosition) in order to determine whether to animate the views up from the bottom (if scrolling down) or down from the top (if we're scrolling up).
The wonderful thing is, you can do so much more by just modifying the initial convertView properties (e.g. convertView.setScaleX(float scale)) and the convertView.animate() chain (e.g. .scaleX(float)).
Try this by putting this in your getView() method Just before returning your convertView:
Animation animationY = new TranslateAnimation(0, 0, holder.llParent.getHeight()/4, 0);
animationY.setDuration(1000);
Yourconvertview.startAnimation(animationY);
animationY = null;
Where llParent = RootLayout which consists your Custom Row Item.
It's honestly going to be a lot of work and quite mathematically intense, but I would have thought you could make the list item's layouts have padding top and bottom and that you could adjust that padding for each item so that the individual items become more or less spaced out. How you would track by how much and how you would know the speed at which the items are being scrolled, well that would be the hard part.
Since we do want items to pop every time they appear at the top or bottom of our list, the best place to do it is the getView() method of the adapter:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
animatePostHc(position, v);
} else {
animatePreHc(position, v);
}
From what I understand what you are looking for is a parallax effect.
This answer is really complete and I think that can help you a lot.
Use this library: http://nhaarman.github.io/ListViewAnimations
It is very awesome. Better than the iOS in atleast it is open source :)
I want to implement a Gallery that allows the user to drag items out of it. This shouldn't get in the way of scrolling/flinging.
Given the interface layout, the user can only drag items out of the Gallery in a vertical path, and scroll the Gallery horizontally.
Is this feasible? Is there an easy way of detecting horizontal movements, and defer them to the Gallery's event handlers, and intercept vertical movements? Or do I have to override onInterceptTouchEvent() and do the math myself?
(edit: I'm giving a try to a GestureListener, overriding onFling and onScroll, and passing the events to the Gallery when the vertical scroll distance is below a threshold)
I inherited Gallery, and overrode the onScroll method. I haven't implemented the drop logic yet, but the dragging and scrolling work.
When I can spare the time, I'll write a full post in my blog with more details, and the drop mechanism. For now, a simple copy-paste in case somebody reaches this page in the future.
To keep the behavior where it belongs, I created this DraggableView interface:
public interface DraggableView {
public void beforeDrag();
public DragView createDragView();
public Object getDraggedInfo();
public void afterDrop();
}
Views in the Gallery can be dragged out of the Gallery area if they implement this view. They are notified before and after, and must implement two methods:
createDragView() returns a DragView object. Basically, a transparent hovering bitmap to accompany the user's movement.
getDraggedInfo() returns the information that should reach the drop target.
Here's the DragView class:
public class DragView extends ImageView {
private final LayoutParams mLayoutParams;
public DragView(Context context, Bitmap bitmap) {
super(context);
mLayoutParams = new LayoutParams();
mLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
mLayoutParams.height = LayoutParams.WRAP_CONTENT;
mLayoutParams.width = LayoutParams.WRAP_CONTENT;
mLayoutParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE
| LayoutParams.FLAG_NOT_TOUCHABLE;
mLayoutParams.format = PixelFormat.TRANSLUCENT;
mLayoutParams.windowAnimations = 0;
mLayoutParams.alpha = 0.5f;
setImageBitmap(bitmap);
setLayoutParams(mLayoutParams);
}
public void move(int x, int y) {
mLayoutParams.x = x;
mLayoutParams.y = y;
}
}
As you can see, it takes a Bitmap in construction, and creates a hovering ImageView. Finally, here is the (just implemented and not very clean) Gallery code to make it all happen:
public class DraggableItemGallery extends Gallery {
private boolean mDragging;
private DragView mDragView;
private DraggableView mDragViewOwner;
private WindowManager mWindowManager;
private boolean mScrollStarted;
public DraggableItemGallery(Context context) {
super(context);
initialize();
}
public DraggableItemGallery(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public DraggableItemGallery(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
public void initialize() {
mWindowManager = (WindowManager)
getContext().getSystemService("window");
}
private void startDraggingItem(DraggableView view, int x, int y) {
mDragging = true;
mDragViewOwner = view;
mDragView = view.createDragView();
mDragView.move(x, y);
mWindowManager.addView(mDragView, mDragView.getLayoutParams());
}
private void continueDraggingItem(int x, int y) {
DragView dragView = getDragView();
dragView.move(x, y);
mWindowManager.updateViewLayout(dragView, dragView.getLayoutParams());
}
private void stopDraggingItem() {
mDragging = false;
mWindowManager.removeView(mDragView);
mDragViewOwner.afterDrop();
mDragView = null;
mDragViewOwner = null;
}
private DraggableView getDraggedItem() {
return mDragViewOwner;
}
private DragView getDragView() {
return mDragView;
}
private boolean isDraggingItem() {
return (mDragging);
}
private void setScrolling(boolean scrolling) {
mScrollStarted = scrolling;
System.out.println("Scrolling " + scrolling);
}
private boolean isScrolling() {
return mScrollStarted;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if ((event.getAction() & ACTION_MASK) == ACTION_UP) {
setScrolling(false);
if (isDraggingItem())
stopDraggingItem();
}
return super.onTouchEvent(event);
}
final Rect onScroll_tempRect = new Rect();
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (isScrolling()) {
if (isDraggingItem()) {
int x = (int) e2.getX(),
y = (int) e2.getY();
System.out.println("Moving to " + x + " " + y);
continueDraggingItem(x, y);
return true;
} else {
/* Not dragging, let the Gallery handle the event */
return super.onScroll(e1, e2, distanceX, distanceY);
}
} else {
setScrolling(true);
boolean isVertical = (Math.abs(distanceY) > Math.abs(distanceX));
if (isVertical) {
int x = (int) e1.getX(),
y = (int) e1.getY();
View hitChild = null;
// A tiny optimization, declared above this method
final Rect hitRect = onScroll_tempRect;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.getHitRect(hitRect);
if (hitRect.contains(x, y)) {
hitChild = child;
break;
}
}
if (hitChild instanceof DraggableView) {
startDraggingItem((DraggableView) hitChild, x, y);
return true;
}
}
/* Either the scroll is not vertical, or the point
* of origin is not above a DraggableView. Again,
* we let the Gallery handle the event.
*/
return super.onScroll(e1, e2, distanceX, distanceY);
}
}
}
Hope it helps.
Here is something I did to do exactly that. That's only the code for the activity... there is some layout and other res files you'll need...
Every list item has an icon and name matched randomly.
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.widget.FrameLayout.LayoutParams;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import java.util.ArrayList;
import java.util.Arrays;
public class DragActivity extends Activity implements View.OnTouchListener, AdapterView.OnItemLongClickListener
{
private static final String TAG="DragActivity";
private static final int NOT_DRAGGING = 0;
private static final int DRAGGING = 1;
private int state=NOT_DRAGGING;
private ImageView draggable =null;
private int dragged_position;
float current_x, current_y;
int current_icon = R.drawable.notepad;
private ArrayList<String> names = new ArrayList<String>(Arrays.asList("John", "Mark", "Mathew", "Luke", "Bob", "Will", "Brian", "Mike"));
private ArrayList<Integer> icons = new ArrayList<Integer>(Arrays.asList( R.drawable.glasses, R.drawable.monkey, R.drawable.normal, R.drawable.smile, R.drawable.wink));
private ArrayList<Integer> matching;
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
setupListContent();
ListView list = (ListView) findViewById(R.id.main_list);
list.setAdapter(new DragListAdapter());
list.setOnItemLongClickListener(this);
list.setOnTouchListener(this);
// need to use the same view for the both listeners, as described in Android documentation :
// http://developer.android.com/guide/topics/ui/ui-events.html
// onTouch() - This returns a boolean to indicate whether your listener consumes this event. The important thing
// is that this event can have multiple actions that follow each other. So, if you return false when the down action
// event is received, you indicate that you have not consumed the event and are also not interested in subsequent
// actions from this event. Thus, you will not be called for any other actions within the event, such as a finger
// gesture, or the eventual up action event.
ImageView image = (ImageView) findViewById(R.id.main_image);
image.setImageResource(current_icon);
}
private void setupListContent() {
matching = new ArrayList<Integer>();
for (int i=0; i<names.size(); i++) {
matching.add((int) (icons.size() * Math.random()));
}
}
#SuppressWarnings("unchecked")
private class DragListAdapter extends ArrayAdapter {
public DragListAdapter() {
super(DragActivity.this, R.layout.list_item, names);
}
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
if (row == null) {
LayoutInflater inflater = getLayoutInflater();
row = inflater.inflate(R.layout.list_item, parent, false);
}
row.setDrawingCacheEnabled(true);
TextView name = (TextView) row.findViewById(R.id.item_text);
ImageView icon = (ImageView) row.findViewById(R.id.item_icon);
name.setText(names.get(position));
icon.setImageResource(icons.get(matching.get(position)));
return row;
}
}
private boolean checkOnDropIcon(MotionEvent me) {
ImageView drop_icon = (ImageView) findViewById(R.id.main_image);
Rect icon_rect = new Rect();
drop_icon.getGlobalVisibleRect(icon_rect);
Log.d(TAG, "icon at " + icon_rect.left + "<- ->" + icon_rect.right + ", " +
icon_rect.top + " ^ v" + icon_rect.bottom);
if ((me.getRawX()<icon_rect.left) || (me.getRawX()>icon_rect.right) ||
(me.getRawY()<icon_rect.top) || (me.getRawY()>icon_rect.bottom)) {
return false;
}
else {
return true;
}
}
private void checkOnDrop(MotionEvent me) {
boolean onDropIcon = checkOnDropIcon(me);
ImageView image = (ImageView) findViewById(R.id.main_image);
if ((onDropIcon) && (current_icon==R.drawable.notepad)) {
current_icon = R.drawable.exit;
image.setImageResource(current_icon);
image.invalidate();
return;
}
if ((!onDropIcon) && (current_icon==R.drawable.exit)) {
current_icon = R.drawable.notepad;
image.setImageResource(current_icon);
image.invalidate();
return;
}
}
public boolean onTouch(View view, MotionEvent me) {
if (state == NOT_DRAGGING) {
// get the position of the touch so we know where to place the dragging item if it is a long press
current_x = me.getRawX();
current_y = me.getRawY();
return false;
}
else {
FrameLayout frame = (FrameLayout) findViewById(R.id.drag_space);
if (me.getAction()==MotionEvent.ACTION_UP) {
frame.removeAllViews();
draggable=null;
frame.setVisibility(View.GONE);
state=NOT_DRAGGING;
// check if we dropped a name
if (checkOnDropIcon(me)) {
names.remove(dragged_position);
matching.remove(dragged_position);
ListView list = (ListView) findViewById(R.id.main_list);
DragListAdapter adapter = (DragListAdapter) list.getAdapter();
adapter.notifyDataSetChanged();
}
// restore the icon
ImageView image = (ImageView) findViewById(R.id.main_image);
current_icon = R.drawable.notepad;
image.setImageResource(current_icon);
image.invalidate();
}
if (me.getAction()==MotionEvent.ACTION_MOVE) {
int frame_position[] = new int[2];
frame.getLocationOnScreen(frame_position);
draggable.setPadding(
(int) me.getRawX()-frame_position[0]-(draggable.getDrawable().getIntrinsicWidth()/2),
(int) me.getRawY()-frame_position[1]-(draggable.getDrawable().getIntrinsicHeight()/2),
0, 0);
draggable.invalidate();
checkOnDrop(me);
}
return true;
}
}
public boolean onItemLongClick(AdapterView<?> adapterView, View view, int i, long l) {
if (state == DRAGGING) {
Log.d(TAG, "already have an object moving... ?");
return false;
}
FrameLayout frame = (FrameLayout) findViewById(R.id.drag_space);
int frame_position[] = new int[2];
frame.getLocationOnScreen(frame_position);
// setup everything for dragging
state = DRAGGING;
dragged_position = i;
draggable = new ImageView(this);
Bitmap bm = view.getDrawingCache();
draggable.setImageBitmap(bm);
draggable.setAlpha(150);
draggable.setScaleType(ImageView.ScaleType.CENTER);
draggable.setDrawingCacheEnabled(true);
draggable.setPadding((int) current_x-frame_position[0]-(bm.getWidth()/2), (int) current_y-frame_position[1]-(bm.getHeight()/2), 0, 0);
frame.setVisibility(View.VISIBLE);
frame.addView(draggable, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
return true;
}
}