Currently I have a Button in a layout, however an assigned OnClickListener never calls back to the onClick method.
Is it possible to intercept the click of a Button in a layout assigned to a MarkerView?
I have finished my app with clickable marker view. My solution is that we'll create a subclass of LineChart (or other chart), then let override onTouchEvent and detect the touch location.
public class MyChart extends LineChart {
#Override
public boolean onTouchEvent(MotionEvent event) {
boolean handled = true;
// if there is no marker view or drawing marker is disabled
if (isShowingMarker() && this.getMarker() instanceof ChartInfoMarkerView){
ChartInfoMarkerView markerView = (ChartInfoMarkerView) this.getMarker();
Rect rect = new Rect((int)markerView.drawingPosX,(int)markerView.drawingPosY,(int)markerView.drawingPosX + markerView.getWidth(), (int)markerView.drawingPosY + markerView.getHeight());
if (rect.contains((int) event.getX(),(int) event.getY())) {
// touch on marker -> dispatch touch event in to marker
markerView.dispatchTouchEvent(event);
}else{
handled = super.onTouchEvent(event);
}
}else{
handled = super.onTouchEvent(event);
}
return handled;
}
private boolean isShowingMarker(){
return mMarker != null && isDrawMarkersEnabled() && valuesToHighlight();
}
}
public class ChartInfoMarkerView extends MarkerView {
#BindView(R.id.markerContainerView)
LinearLayout markerContainerView;
protected float drawingPosX;
protected float drawingPosY;
private static final int MAX_CLICK_DURATION = 500;
private long startClickTime;
/**
* The constructor
*
* #param context
* #param layoutResource
*/
public ChartInfoMarkerView(Context context, int layoutResource) {
super(context, layoutResource);
ButterKnife.bind(this);
markerContainerView.setClickable(true);
markerContainerView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
Log.d("MARKER","click");
}
});
}
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
startClickTime = Calendar.getInstance().getTimeInMillis();
break;
}
case MotionEvent.ACTION_UP: {
long clickDuration = Calendar.getInstance().getTimeInMillis() - startClickTime;
if(clickDuration < MAX_CLICK_DURATION) {
markerContainerView.performClick();
}
}
}
return super.onTouchEvent(event);
}
#Override
public void draw(Canvas canvas, float posX, float posY) {
super.draw(canvas, posX, posY);
MPPointF offset = getOffsetForDrawingAtPoint(posX, posY);
this.drawingPosX = posX + offset.x;
this.drawingPosY = posY + offset.y;
}
}
Using the library it appears not to be possible, however a solution of sorts is to show a View or ViewGroup over the chart which has a Button in it. You’ll need to set up an empty layout for the MarkerView and wrap your Chart in a ViewGroup such as a RelativeLayout.
Define a listener such as this in your CustomMarkerView:
public interface Listener {
/**
* A callback with the x,y position of the marker
* #param x the x in pixels
* #param y the y in pixels
*/
void onMarkerViewLayout(int x, int y);
}
Set up some member variables:
private Listener mListener;
private int mLayoutX;
private int mLayoutY;
private int mMarkerVisibility;
In your constructor require a listener:
/**
* Constructor. Sets up the MarkerView with a custom layout resource.
* #param context a context
* #param layoutResource the layout resource to use for the MarkerView
* #param listener listens for the bid now click
*/
public SQUChartMarkerView(Context context, int layoutResource, Listener listener) {
super(context, layoutResource);
mListener = listener;
}
Store the location the marker should be when the values are set:
#Override public int getXOffset(float xpos) {
mLayoutX = (int) (xpos - (getWidth() / 2));
return -getWidth() / 2;
}
#Override public int getYOffset(float ypos) {
mLayoutY = (int) (ypos - getWidth());
return -getHeight();
}
Then override onDraw to determine when you should draw your layout:
#Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mMarkerVisibility == View.VISIBLE) mListener.onMarkerViewLayout(mLayoutX, mLayoutY);
}
I added an option to change the state of the marker:
public void setMarkerVisibility(int markerVisibility) {
mMarkerVisibility = markerVisibility;
}
Where you listen for marker being laid out, get your layout (eg. inflate it or you may have it as a member variable), make sure you measure it, then set the margins. In this case I am using getParent() as the chart and the layout for the marker share the same parent. I have a BarChart so my margins may be different from yours.
#Override public void onMarkerViewLayout(int x, int y) {
if(getParent() == null || mChartListener.getAmount() == null) return;
// remove the marker
((ViewGroup) getParent()).removeView(mMarkerLayout);
((ViewGroup) getParent()).addView(mMarkerLayout);
// measure the layout
// if this is not done, the first calculation of the layout parameter margins
// will be incorrect as the layout at this point has no height or width
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(((ViewGroup) getParent()).getWidth(), View.MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(((ViewGroup) getParent()).getHeight(), View.MeasureSpec.UNSPECIFIED);
mMarkerLayout.measure(widthMeasureSpec, heightMeasureSpec);
// set up layout parameters so our marker is in the same position as the mpchart marker would be (based no the x and y)
RelativeLayout.LayoutParams lps = (RelativeLayout.LayoutParams) mMarkerLayout.getLayoutParams();
lps.height = FrameLayout.LayoutParams.WRAP_CONTENT;
lps.width = FrameLayout.LayoutParams.WRAP_CONTENT;
lps.leftMargin = x - mMarkerLayout.getMeasuredWidth() / 2;
lps.topMargin = y - mMarkerLayout.getMeasuredHeight();
}
Hope this helps.
Related
I'm using "subsampling scale image view" in my app and I would like to dynamically add marker pins to a PinView when the user does a long click on it. The marker pin should appear at the clicked position.
I achieved that a marker pin appears after a long click, but at a wrong position. Here is the onCreateView method of my Fragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
pinCounter=0;
View rootView = inflater.inflate(R.layout.fragment_edit_plan, container, false);
src = getArguments().getString("src");
imageView = (PinView)rootView.findViewById(R.id.imageView);
imageView.setImage(ImageSource.uri(src));
imageView.isLongClickable();
imageView.isClickable();
imageView.hasOnClickListeners();
MapPins = new ArrayList();
imageView.setPins(MapPins);
imageView.setOnLongClickListener(this);
imageView.setOnTouchListener(this);
return rootView;
}
The Listener-Methods:
#Override
public boolean onLongClick(View view) {
if (view.getId() == R.id.imageView) {
Toast.makeText(getActivity(), "Long clicked "+lastKnownX+" "+lastKnownY, Toast.LENGTH_SHORT).show();
Log.d("EditPlanFragment","Scale "+imageView.getScale());
MapPins.add(new MapPin(lastKnownX,lastKnownY,pinCounter));
imageView.setPins(MapPins);
imageView.post(new Runnable(){
public void run(){
imageView.getRootView().postInvalidate();
}
});
return true;
}
return false;
}
#Override
public boolean onTouch(View v, MotionEvent event) {
if (v.getId()== R.id.imageView && event.getAction() == MotionEvent.ACTION_DOWN){
lastKnownX= event.getX();
lastKnownY= event.getY();
}
return false;
}
My XML-File:
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="50dp"
>
<com.example.viktoriabock.phoenixversion1.PinView
android:id="#+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:longClickable="true"
/>
And the PinView-Class:
public class PinView extends SubsamplingScaleImageView {
private PointF sPin;
ArrayList<MapPin> mapPins;
ArrayList<DrawPin> drawnPins;
Context context;
String tag = getClass().getSimpleName();
public PinView(Context context) {
this(context, null);
this.context = context;
}
public PinView(Context context, AttributeSet attr) {
super(context, attr);
this.context = context;
initialise();
}
public void setPins(ArrayList<MapPin> mapPins) {
this.mapPins = mapPins;
initialise();
invalidate();
}
public void setPin(PointF pin) {
this.sPin = pin;
}
public PointF getPin() {
return sPin;
}
private void initialise() {
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Don't draw pin before image is ready so it doesn't move around during setup.
if (!isReady()) {
return;
}
drawnPins = new ArrayList<>();
Paint paint = new Paint();
paint.setAntiAlias(true);
float density = getResources().getDisplayMetrics().densityDpi;
for (int i = 0; i < mapPins.size(); i++) {
MapPin mPin = mapPins.get(i);
//Bitmap bmpPin = Utils.getBitmapFromAsset(context, mPin.getPinImgSrc());
Bitmap bmpPin = BitmapFactory.decodeResource(this.getResources(), R.drawable.pushpin_blue);
float w = (density / 420f) * bmpPin.getWidth();
float h = (density / 420f) * bmpPin.getHeight();
bmpPin = Bitmap.createScaledBitmap(bmpPin, (int) w, (int) h, true);
PointF vPin = sourceToViewCoord(mPin.getPoint());
//in my case value of point are at center point of pin image, so we need to adjust it here
float vX = vPin.x - (bmpPin.getWidth() / 2);
float vY = vPin.y - bmpPin.getHeight();
canvas.drawBitmap(bmpPin, vX, vY, paint);
//add added pin to an Array list to get touched pin
DrawPin dPin = new DrawPin();
dPin.setStartX(mPin.getX() - w / 2);
dPin.setEndX(mPin.getX() + w / 2);
dPin.setStartY(mPin.getY() - h / 2);
dPin.setEndY(mPin.getY() + h / 2);
dPin.setId(mPin.getId());
drawnPins.add(dPin);
}
}
public int getPinIdByPoint(PointF point) {
for (int i = drawnPins.size() - 1; i >= 0; i--) {
DrawPin dPin = drawnPins.get(i);
if (point.x >= dPin.getStartX() && point.x <= dPin.getEndX()) {
if (point.y >= dPin.getStartY() && point.y <= dPin.getEndY()) {
return dPin.getId();
}
}
}
return -1; //negative no means no pin selected
}
}
You're collecting the screen coordinates of the long press, then in the custom view you're converting them from source coordinates to screen coordinates. You need to convert the event coordinates into source coordinates when you get the tap event, using the view's viewToSourceCoord method.
There's an easier way to do this than using your lastKnown values - use your own GestureDetector.
For a full working example of a long press gesture detector, see the AdvancedEventHandlingActivity sample class.
Hi I am using the Gallery widget to show images downloaded from the internet.
to show several images and I would like to have a gradual zoom while people slide up and down on the screen. I know how to implement the touch event the only thing I don't know how to make the whole gallery view grow gradually. I don't want to zoom in on one image I want the whole gallery to zoom in/out gradually.
EDIT3: I manage to zoom the visible part of the gallery but the problem is I need to find a way for the gallery to find out about it and update it's other children too.
What happens is if 3 images are visible then you start zooming and the gallery does get smaller, so do the images but what I would like in this case is more images to be visible but I don't know how to reach this desired effect. Here's the entire code:
public class Gallery1 extends Activity implements OnTouchListener {
private static final String TAG = "GalleryTest";
private float zoom=0.0f;
// Remember some things for zooming
PointF start = new PointF();
PointF mid = new PointF();
Gallery g;
LinearLayout layout2;
private ImageAdapter ad;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.gallery_1);
layout2=(LinearLayout) findViewById(R.id.layout2);
// Reference the Gallery view
g = (Gallery) findViewById(R.id.gallery);
// Set the adapter to our custom adapter (below)
ad=new ImageAdapter(this);
g.setAdapter(ad);
layout2.setOnTouchListener(this);
}
public void zoomList(boolean increase) {
Log.i(TAG, "startig animation");
AnimatorSet set = new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(g, "scaleX", zoom),
ObjectAnimator.ofFloat(g, "scaleY", zoom)
);
set.addListener(new AnimatorListener() {
#Override
public void onAnimationStart(Animator animation) {
}
#Override
public void onAnimationRepeat(Animator animation) {
// TODO Auto-generated method stub
}
#Override
public void onAnimationEnd(Animator animation) {
}
#Override
public void onAnimationCancel(Animator animation) {
// TODO Auto-generated method stub
}
});
set.setDuration(100).start();
}
public class ImageAdapter extends BaseAdapter {
private static final int ITEM_WIDTH = 136;
private static final int ITEM_HEIGHT = 88;
private final int mGalleryItemBackground;
private final Context mContext;
private final Integer[] mImageIds = {
R.drawable.gallery_photo_1,
R.drawable.gallery_photo_2,
R.drawable.gallery_photo_3,
R.drawable.gallery_photo_4,
R.drawable.gallery_photo_5,
R.drawable.gallery_photo_6,
R.drawable.gallery_photo_7,
R.drawable.gallery_photo_8
};
private final float mDensity;
public ImageAdapter(Context c) {
mContext = c;
// See res/values/attrs.xml for the <declare-styleable> that defines
// Gallery1.
TypedArray a = obtainStyledAttributes(R.styleable.Gallery1);
mGalleryItemBackground = a.getResourceId(
R.styleable.Gallery1_android_galleryItemBackground, 1);
a.recycle();
mDensity = c.getResources().getDisplayMetrics().density;
}
public int getCount() {
return mImageIds.length;
}
public Object getItem(int position) {
return position;
}
public long getItemId(int position) {
return position;
}
public View getView(int position, View convertView, ViewGroup parent) {
ImageView imageView;
if (convertView == null) {
convertView = new ImageView(mContext);
imageView = (ImageView) convertView;
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
imageView.setLayoutParams(new Gallery.LayoutParams(
(int) (ITEM_WIDTH * mDensity + 0.5f),
(int) (ITEM_HEIGHT * mDensity + 0.5f)));
} else {
imageView = (ImageView) convertView;
}
imageView.setImageResource(mImageIds[position]);
return imageView;
}
}
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_MOVE
&& event.getPointerCount() > 1) {
midPoint(mid, event);
if(mid.y > start.y){
Log.i(TAG, "Going down (Math.abs(mid.y - start.y)= "+(Math.abs(mid.y - start.y))+" and zoom="+zoom); // going down so increase
if ((Math.abs(mid.y - start.y) > 10) && (zoom<2.5f)){
zoom=zoom+0.1f;
midPoint(start, event);
zoomList(true);
}
return true;
}else if(mid.y < start.y){
Log.i(TAG, "Going up (Math.abs(mid.y - start.y)= "+(Math.abs(mid.y - start.y))+" and zoom="+zoom); //smaller
if ((Math.abs(mid.y - start.y) > 10) &&(zoom>0.1)){
midPoint(start, event);
zoom=zoom-0.1f;
zoomList(false);
}
return true;
}
}
else if (event.getAction() == MotionEvent.ACTION_POINTER_DOWN) {
Log.e(TAG, "Pointer went down: " + event.getPointerCount());
return true;
}
else if (event.getAction() == MotionEvent.ACTION_UP) {
Log.i(TAG, "Pointer going up");
return true;
}
else if (event.getAction() == MotionEvent.ACTION_DOWN) {
Log.i(TAG, "Pointer going down");
start.set(event.getX(), event.getY());
return true;
}
return false;
// indicate event was handled or not
}
private void midPoint(PointF point, MotionEvent event) {
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
}
I realise I will probably have to extend the Gallery or even another View group or create my own class but I don't know where to start: which method use the one responsible for scaling...
EDIT4: I don't know if he question is clear enough. Here is an example of states:
State one: initial state, we have 3 images in view
State 2: we detect vertical touches going up with 2 fingers = we have to zoom out
state 3: we start zooming = animation on the gallery or on the children???
state 4: gallery detects that it's 3 children are smaller
state 5: gallery adds 1 /more children according to the new available space
LAST UPDATE:
Thanks to all that have posted but I have finally reached a conclusion and that is to not use Gallery at all:
1. It's deprecated
2. It's not customizable enough for my case
If you want to animate several images at once you may want to consider using OpenGl, I am using libgdx library:
https://github.com/libgdx/libgdx
The following ScalingGallery implementation might be of help.
This gallery subclass overrides the getChildStaticTransformation(View child, Transformation t) method in which the scaling is performed. You can further customize the scaling parameters to fit your own needs.
Please note the ScalingGalleryItemLayout.java class. This is necessary because after you have performed the scaling operationg on the child views, their hit boxes are no longer valid so they must be updated from with the getChildStaticTransformation(View child, Transformation t) method.
This is done by wrapping each gallery item in a ScalingGalleryItemLayout which extends a LinearLayout. Again, you can customize this to fit your own needs if a LinearLayout does not meet your needs for layout out your gallery items.
File : /src/com/example/ScalingGallery.java
/**
* A Customized Gallery component which alters the size and position of its items based on their position in the Gallery.
*/
public class ScalingGallery extends Gallery {
public static final int ITEM_SPACING = -20;
private static final float SIZE_SCALE_MULTIPLIER = 0.25f;
private static final float ALPHA_SCALE_MULTIPLIER = 0.5f;
private static final float X_OFFSET = 20.0f;
/**
* Implemented by child view to adjust the boundaries after it has been matrix transformed.
*/
public interface SetHitRectInterface {
public void setHitRect(RectF newRect);
}
/**
* #param context
* Context that this Gallery will be used in.
* #param attrs
* Attributes for this Gallery (via either xml or in-code)
*/
public ScalingGallery(Context context, AttributeSet attrs) {
super(context, attrs);
setStaticTransformationsEnabled(true);
setChildrenDrawingOrderEnabled(true);
}
/**
* {#inheritDoc}
*
* #see #setStaticTransformationsEnabled(boolean)
*
* This is where the scaling happens.
*/
protected boolean getChildStaticTransformation(View child, Transformation t) {
child.invalidate();
t.clear();
t.setTransformationType(Transformation.TYPE_BOTH);
// Position of the child in the Gallery (... +2 +1 0 -1 -2 ... 0 being the middle)
final int childPosition = getSelectedItemPosition() - getPositionForView(child);
final int childPositionAbs = (int) Math.abs(childPosition);
final float left = child.getLeft();
final float top = child.getTop();
final float right = child.getRight();
final float bottom = child.getBottom();
Matrix matrix = t.getMatrix();
RectF modifiedHitBox = new RectF();
// Change alpha, scale and translate non-middle child views.
if (childPosition != 0) {
final int height = child.getMeasuredHeight();
final int width = child.getMeasuredWidth();
// Scale the size.
float scaledSize = 1.0f - (childPositionAbs * SIZE_SCALE_MULTIPLIER);
if (scaledSize < 0) {
scaledSize = 0;
}
matrix.setScale(scaledSize, scaledSize);
float moveX = 0;
float moveY = 0;
// Moving from right to left -- linear move since the scaling is done with respect to top-left corner of the view.
if (childPosition < 0) {
moveX = ((childPositionAbs - 1) * SIZE_SCALE_MULTIPLIER * width) + X_OFFSET;
moveX *= -1;
} else { // Moving from left to right -- sum of the previous positions' x displacements.
// X(n) = X(0) + X(1) + X(2) + ... + X(n-1)
for (int i = childPositionAbs; i > 0; i--) {
moveX += (i * SIZE_SCALE_MULTIPLIER * width);
}
moveX += X_OFFSET;
}
// Moving down y-axis is linear.
moveY = ((childPositionAbs * SIZE_SCALE_MULTIPLIER * height) / 2);
matrix.postTranslate(moveX, moveY);
// Scale alpha value.
final float alpha = (1.0f / childPositionAbs) * ALPHA_SCALE_MULTIPLIER;
t.setAlpha(alpha);
// Calculate new hit box. Since we moved the child, the hitbox is no longer lined up with the new child position.
final float newLeft = left + moveX;
final float newTop = top + moveY;
final float newRight = newLeft + (width * scaledSize);
final float newBottom = newTop + (height * scaledSize);
modifiedHitBox = new RectF(newLeft, newTop, newRight, newBottom);
} else {
modifiedHitBox = new RectF(left, top, right, bottom);
}
// update child hit box so you can tap within the child's boundary
((SetHitRectInterface) child).setHitRect(modifiedHitBox);
return true;
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// Helps to smooth out jittering during scrolling.
// read more - http://www.unwesen.de/2011/04/17/android-jittery-scrolling-gallery/
final int viewsOnScreen = getLastVisiblePosition() - getFirstVisiblePosition();
if (viewsOnScreen <= 0) {
super.onLayout(changed, l, t, r, b);
}
}
private int mLastDrawnPosition;
#Override
protected int getChildDrawingOrder(int childCount, int i) {
//Reset the last position variable every time we are starting a new drawing loop
if (i == 0) {
mLastDrawnPosition = 0;
}
final int centerPosition = getSelectedItemPosition() - getFirstVisiblePosition();
if (i == childCount - 1) {
return centerPosition;
} else if (i >= centerPosition) {
mLastDrawnPosition++;
return childCount - mLastDrawnPosition;
} else {
return i;
}
}
}
File : /src/com/example/ScalingGalleryItemLayout.java
public class ScalingGalleryItemLayout extends LinearLayout implements SetHitRectInterface {
public ScalingGalleryItemLayout(Context context) {
super(context);
}
public ScalingGalleryItemLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScalingGalleryItemLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private Rect mTransformedRect;
#Override
public void setHitRect(RectF newRect) {
if (newRect == null) {
return;
}
if (mTransformedRect == null) {
mTransformedRect = new Rect();
}
newRect.round(mTransformedRect);
}
#Override
public void getHitRect(Rect outRect) {
if (mTransformedRect == null) {
super.getHitRect(outRect);
} else {
outRect.set(mTransformedRect);
}
}
}
File : /res/layout/ScaledGalleryItemLayout.xml
<?xml version="1.0" encoding="utf-8"?>
<com.example.ScalingGalleryItemLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/gallery_item_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
android:padding="5dp" >
<ImageView
android:id="#+id/gallery_item_image"
android:layout_width="360px"
android:layout_height="210px"
android:layout_gravity="center"
android:antialias="true"
android:background="#drawable/gallery_item_button_selector"
android:cropToPadding="true"
android:padding="35dp"
android:scaleType="centerInside" />
<TextView
android:id="#+id/gallery_item_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#drawable/white"
android:textSize="30sp" />
</com.example.ScalingGalleryItemLayout>
To keep the state of the animation after it is done, just do this on your animation:
youranim.setFillAfter(true);
Edit :
In my project, I use this method and i think, it's help you :
http://developer.sonymobile.com/wp/2011/04/12/how-to-take-advantage-of-the-pinch-to-zoom-feature-in-your-xperia%E2%84%A2-10-apps-part-1/
U can do Image Zoom pinch option for gallery also.
by using below code lines:
you can download the example.
https://github.com/alvinsj/android-image-gallery/downloads
I hope this example will help to u..if u have any queries ask me.....
This is solution
integrate gallery component in android with gesture-image library
gesture-imageView
And here is full sample code
SampleCode
I am developing an application, and I need a ListView like conctact ListView of my Samsung Galaxy S:
When I slide my finger to the right I can send message to this contact.
When I slide my finger to the right I can call to my contact.
I have my ListView and only need the function for do it...
Thanks in advance.
PD:
I searched a lot and have not found anything. The most similar:
Resource for Android Slight Left/Right Slide action on listview
From another post, there was a link to this Google Code : https://gist.github.com/2980593
Which come from this Google+ post : https://plus.google.com/u/0/113735310430199015092/posts/Fgo1p5uWZLu .
This is a Swipe-To-Dismiss functionality.
From this you can provide your own Swipe-To-Action code. So here is my version, were I can personalize the left and right action and you can triggered the Dismiss animation (this is just a modification of Roman Nuric's code).
You have to include this class in your project :
public class SwipeListViewTouchListener implements View.OnTouchListener {
// Cached ViewConfiguration and system-wide constant values
private int mSlop;
private int mMinFlingVelocity;
private int mMaxFlingVelocity;
private long mAnimationTime;
// Fixed properties
private ListView mListView;
private OnSwipeCallback mCallback;
private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero
private boolean dismissLeft = true;
private boolean dismissRight = true;
// Transient properties
private List < PendingSwipeData > mPendingSwipes = new ArrayList < PendingSwipeData > ();
private int mDismissAnimationRefCount = 0;
private float mDownX;
private boolean mSwiping;
private VelocityTracker mVelocityTracker;
private int mDownPosition;
private View mDownView;
private boolean mPaused;
/**
* The callback interface used by {#link SwipeListViewTouchListener} to inform its client
* about a successful swipe of one or more list item positions.
*/
public interface OnSwipeCallback {
/**
* Called when the user has swiped the list item to the left.
*
* #param listView The originating {#link ListView}.
* #param reverseSortedPositions An array of positions to dismiss, sorted in descending
* order for convenience.
*/
void onSwipeLeft(ListView listView, int[] reverseSortedPositions);
void onSwipeRight(ListView listView, int[] reverseSortedPositions);
}
/**
* Constructs a new swipe-to-action touch listener for the given list view.
*
* #param listView The list view whose items should be dismissable.
* #param callback The callback to trigger when the user has indicated that she would like to
* dismiss one or more list items.
*/
public SwipeListViewTouchListener(ListView listView, OnSwipeCallback callback) {
ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
mSlop = vc.getScaledTouchSlop();
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
mAnimationTime = listView.getContext().getResources().getInteger(
android.R.integer.config_shortAnimTime);
mListView = listView;
mCallback = callback;
}
/**
* Constructs a new swipe-to-action touch listener for the given list view.
*
* #param listView The list view whose items should be dismissable.
* #param callback The callback to trigger when the user has indicated that she would like to
* dismiss one or more list items.
* #param dismissLeft set if the dismiss animation is up when the user swipe to the left
* #param dismissRight set if the dismiss animation is up when the user swipe to the right
* #see #SwipeListViewTouchListener(ListView, OnSwipeCallback, boolean, boolean)
*/
public SwipeListViewTouchListener(ListView listView, OnSwipeCallback callback, boolean dismissLeft, boolean dismissRight) {
this(listView, callback);
this.dismissLeft = dismissLeft;
this.dismissRight = dismissRight;
}
/**
* 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;
}
/**
* Returns an {#link android.widget.AbsListView.OnScrollListener} to be added to the
* {#link ListView} using
* {#link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}.
* If a scroll listener is already assigned, the caller should still pass scroll changes
* through to this listener. This will ensure that this
* {#link SwipeListViewTouchListener} is paused during list view scrolling.</p>
*
* #see {#link SwipeListViewTouchListener}
*/
public AbsListView.OnScrollListener makeScrollListener() {
return new AbsListView.OnScrollListener() {#
Override
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
#
Override
public void onScroll(AbsListView absListView, int i, int i1, int i2) {}
};
}
#
Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (mViewWidth < 2) {
mViewWidth = mListView.getWidth();
}
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
{
if (mPaused) {
return false;
}
// TODO: ensure this is a finger, and set a flag
// Find the child view that was touched (perform a hit test)
Rect rect = new Rect();
int childCount = mListView.getChildCount();
int[] listViewCoords = new int[2];
mListView.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 = mListView.getChildAt(i);
child.getHitRect(rect);
if (rect.contains(x, y)) {
mDownView = child;
break;
}
}
if (mDownView != null) {
mDownX = motionEvent.getRawX();
mDownPosition = mListView.getPositionForView(mDownView);
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(motionEvent);
}
view.onTouchEvent(motionEvent);
return true;
}
case MotionEvent.ACTION_UP:
{
if (mVelocityTracker == null) {
break;
}
float deltaX = motionEvent.getRawX() - mDownX;
mVelocityTracker.addMovement(motionEvent);
mVelocityTracker.computeCurrentVelocity(500); // 1000 by defaut but it was too much
float velocityX = Math.abs(mVelocityTracker.getXVelocity());
float velocityY = Math.abs(mVelocityTracker.getYVelocity());
boolean swipe = false;
boolean swipeRight = false;
if (Math.abs(deltaX) > mViewWidth / 2) {
swipe = true;
swipeRight = deltaX > 0;
} else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity && velocityY < velocityX) {
swipe = true;
swipeRight = mVelocityTracker.getXVelocity() > 0;
}
if (swipe) {
// sufficent swipe value
final View downView = mDownView; // mDownView gets null'd before animation ends
final int downPosition = mDownPosition;
final boolean toTheRight = swipeRight;
++mDismissAnimationRefCount;
mDownView.animate()
.translationX(swipeRight ? mViewWidth : -mViewWidth)
.alpha(0)
.setDuration(mAnimationTime)
.setListener(new AnimatorListenerAdapter() {#
Override
public void onAnimationEnd(Animator animation) {
performSwipeAction(downView, downPosition, toTheRight, toTheRight ? dismissRight : dismissLeft);
}
});
} else {
// cancel
mDownView.animate()
.translationX(0)
.alpha(1)
.setDuration(mAnimationTime)
.setListener(null);
}
mVelocityTracker = null;
mDownX = 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;
if (Math.abs(deltaX) > mSlop) {
mSwiping = true;
mListView.requestDisallowInterceptTouchEvent(true);
// Cancel ListView's touch (un-highlighting the item)
MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
(motionEvent.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
mListView.onTouchEvent(cancelEvent);
}
if (mSwiping) {
mDownView.setTranslationX(deltaX);
mDownView.setAlpha(Math.max(0f, Math.min(1f,
1f - 2f * Math.abs(deltaX) / mViewWidth)));
return true;
}
break;
}
}
return false;
}
class PendingSwipeData implements Comparable < PendingSwipeData > {
public int position;
public View view;
public PendingSwipeData(int position, View view) {
this.position = position;
this.view = view;
}
#
Override
public int compareTo(PendingSwipeData other) {
// Sort by descending position
return other.position - position;
}
}
private void performSwipeAction(final View swipeView, final int swipePosition, boolean toTheRight, boolean dismiss) {
// 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 = swipeView.getLayoutParams();
final int originalHeight = swipeView.getHeight();
final boolean swipeRight = toTheRight;
ValueAnimator animator;
if (dismiss)
animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(mAnimationTime);
else
animator = ValueAnimator.ofInt(originalHeight, 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(mPendingSwipes);
int[] swipePositions = new int[mPendingSwipes.size()];
for (int i = mPendingSwipes.size() - 1; i >= 0; i--) {
swipePositions[i] = mPendingSwipes.get(i).position;
}
if (swipeRight)
mCallback.onSwipeRight(mListView, swipePositions);
else
mCallback.onSwipeLeft(mListView, swipePositions);
ViewGroup.LayoutParams lp;
for (PendingSwipeData pendingDismiss: mPendingSwipes) {
// Reset view presentation
pendingDismiss.view.setAlpha(1f);
pendingDismiss.view.setTranslationX(0);
lp = pendingDismiss.view.getLayoutParams();
lp.height = originalHeight;
pendingDismiss.view.setLayoutParams(lp);
}
mPendingSwipes.clear();
}
}
});
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {#
Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
lp.height = (Integer) valueAnimator.getAnimatedValue();
swipeView.setLayoutParams(lp);
}
});
mPendingSwipes.add(new PendingSwipeData(swipePosition, swipeView));
animator.start();
}
}
From there, you can add the following code to your onCreate in the Activity with the ListView :
// Create a ListView-specific touch listener. ListViews are given special treatment because
// by default they handle touches for their list items... i.e. they're in charge of drawing
// the pressed state (the list selector), handling list item clicks, etc.
SwipeListViewTouchListener touchListener =
new SwipeListViewTouchListener(
listView,
new SwipeListViewTouchListener.OnSwipeCallback() {
#Override
public void onSwipeLeft(ListView listView, int [] reverseSortedPositions) {
// Log.i(this.getClass().getName(), "swipe left : pos="+reverseSortedPositions[0]);
// TODO : YOUR CODE HERE FOR LEFT ACTION
}
#Override
public void onSwipeRight(ListView listView, int [] reverseSortedPositions) {
// Log.i(ProfileMenuActivity.class.getClass().getName(), "swipe right : pos="+reverseSortedPositions[0]);
// TODO : YOUR CODE HERE FOR RIGHT ACTION
}
},
true, // example : left action = dismiss
false); // example : right action without dismiss animation
listView.setOnTouchListener(touchListener);
// Setting this scroll listener is required to ensure that during ListView scrolling,
// we don't look for swipes.
listView.setOnScrollListener(touchListener.makeScrollListener());
Edit:
To add a color modification while swiping, your code must be in the case MotionEvent.ACTION_MOVE close to the mDownView.setAlpha.
What you might what to do here is create a new view especially for the list view (call it ListViewFlinger or something). Then in this view, override its onTouchEvent method and place some code in there to determine a slide gesture. Once you have the slide gesture, fire a onSlideComplete event (you'll have to make that listener) an voialla, you a ListView with slide activated content.
float historicX = Float.NaN, historicY = Float.NaN;
static final TRIGGER_DELTA = 50; // Number of pixels to travel till trigger
#Override public boolean onTouchEvent(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
historicX = e.getX();
historicY = e.getY();
break;
case MotionEvent.ACTION_UP:
if (e.getX() - historicX > -TRIGGER_DELTA) {
onSlideComplete(Direction.LEFT);
return true;
}
else if (e.getX() - historicX > TRIGGER_DELTA) {
onSlideComplete(Direction.RIGHT);
return true;
} break;
default:
return super.onTouchEvent(e);
}
}
enum Direction {
LEFT, RIGHT;
}
interface OnSlideCompleteListener {
void onSlideComplete(Direction dir);
}
If you want to Perform an action On Swiping:
Check out SwipeActionAdapter
It's an awesome library that allows Swipe in both directions with an underlying Layout or Color, and performs a desired action when the swipe/slide gesture is done. You can configure it to reveal/change the layout.
~ I haven't used the Samsung Contacts app, but sounds like this is what you want
If you want to swipe to Reveal actionable buttons:
Check out SwipeMenuListView
In a sense, it is more like the Swipe-able TableViews in iOS.
I am using an EditText widget and would like to modify the context menu that is displayed when the user long presses the view. The problem that I am having is that I need to know the character position within the text of the long press so I can determine what I need to add to the context menu. The base class is doing this because one of the choices in the menu is 'Add “word_clicked_on” To Dictionary.' Setting ClickableSpans within the text does not appear to be a solution since it consumes the click event which makes it impossible to move the edit cursor within the spans.
Here is the solution that I came up with and it does work so I wanted to share it:
First I concluded that I needed to extend the EditText class so that I could intercept the onTouchEvent, capture the ACTION_DOWN event, and save the position. Now that I have the position of the down point I can call getOffsetForPosition(downPointX, downPointY) and get the character position of the long-press. There is one big problem, getOffsetForPosition was not added until SDK 14! To make this solution work I had to back port the functionality of getOffsetForPosition and branch if the current SDK is earlier than SDK_INT 14. Here is the source code for the new class:
public class ScrapEditText extends EditText{
protected float LastDownPositionX, LastDownPositionY;
public ScrapEditText(Context context)
{
super(context);
}
public ScrapEditText(Context context, AttributeSet attrs)
{
super(context, attrs);
}
#Override
public boolean onTouchEvent(MotionEvent event)
{
final int action = event.getActionMasked();
if(action == MotionEvent.ACTION_DOWN)
{
LastDownPositionX = event.getX();
LastDownPositionY = event.getY();
}
return super.onTouchEvent(event);
}
public float GetLastDownPositionX()
{
return LastDownPositionX;
}
public float GetLastDownPositionY()
{
return LastDownPositionY;
}
public int GetOffsetForLastDownPosition()
{
if(Build.VERSION.SDK_INT > 13)
{
// as of SDK 14 the getOffsetForPosition was added to TextView
return getOffsetForPosition(LastDownPositionX, LastDownPositionY);
}
else
{
return GetOffsetForPositionOlderSdk();
}
}
public int GetOffsetForPositionOlderSdk()
{
if (getLayout() == null) return -1;
final int line = GetLineAtCoordinateOlderSDK(LastDownPositionY);
final int offset = GetOffsetAtCoordinateOlderSDK(line, LastDownPositionX);
return offset;
}
public int GetLineAtCoordinateOlderSDK(float y)
{
y -= getTotalPaddingTop();
// Clamp the position to inside of the view.
y = Math.max(0.0f, y);
y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
y += getScrollY();
return getLayout().getLineForVertical((int) y);
}
protected int GetOffsetAtCoordinateOlderSDK(int line, float x) {
x = ConvertToLocalHorizontalCoordinateOlderSDK(x);
return getLayout().getOffsetForHorizontal(line, x);
}
protected float ConvertToLocalHorizontalCoordinateOlderSDK(float x) {
x -= getTotalPaddingLeft();
// Clamp the position to inside of the view.
x = Math.max(0.0f, x);
x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
x += getScrollX();
return x;
}
}
In your Activity derived class:
ScrapText = (ScrapEditText) findViewById(R.id.sample_text);
ScrapText.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener(){
#Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo)
{
int charOffset = FileText.GetOffsetForLastDownPosition();
}
});
I am trying to make a FastSelectEditText, so that:
Text can be selected by long click and slide the finger.
When sliding and selecting, show a magnify glass(like iphone), so that user can see the text under her finger.
Unfortunately there is a problem with my design: The MagGlass shows only inside my FastSelectEditText. When user is selecting text in top lines, she can't see the mag glass.
So I have to use this work around: show the mag glass lower than the finger, when it reaches the top of the FastSelectEditText.
I understand if I use another view for Mag Glass, that won't be a problem. But to keep code simple, I think it's better to keep the Mag Glass inside the FastSelectEditText.
Is there a way to draw something outside the bound of a view?
Or should I write another view(instead of some code inside the customized EditText) to implement a Mag Glass?(And probably put these views inside a frame layout?)
public class FastSelectEditText extends EditText implements OnLongClickListener {
/**
* #param context
*/
public FastSelectEditText(Context context) {
super(context);
init();
}
/**
* #param context
* #param attrs
*/
public FastSelectEditText(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* #param context
* #param attrs
* #param defStyle
*/
public FastSelectEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private MagGlass mMagGlass;
private float mScale;
private void init(){
DisplayMetrics metrics = getResources().getDisplayMetrics();
mScale = metrics.density;
setGravity(Gravity.TOP);
setOnLongClickListener(this);
mMagGlass = new MagGlass();
}
private int getOffset(int x, int y){
Layout layout = getLayout();
int row = layout.getLineForVertical(getScrollY()+y-getPaddingTop());
return layout.getOffsetForHorizontal(row, x-getPaddingLeft());
}
/**
* the position/index when touch down.
*/
private int mDownOffset = 0;
private int mOldSelStart, mOldSelEnd;
/**
* Did the user moved his finger after down event?
*/
private boolean mMoved = false;
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
mMagGlass.setObjectCenter(x, y);
boolean result;
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mOldSelStart = getSelectionStart();
mOldSelEnd = getSelectionEnd();
if (mOldSelStart != mOldSelEnd){
startSlideAndSelect();
}
mDownOffset = getOffset(x, y);
return super.dispatchTouchEvent(event);
case MotionEvent.ACTION_MOVE:
result = super.dispatchTouchEvent(event);
int offset = getOffset(x, y);
if (!mMoved && mDownOffset != offset){
mMoved = true;
}
if (mSlideAndSelect){
if (mMoved){
setSelection(mDownOffset, offset);
}
return true;
}
return result;
case MotionEvent.ACTION_UP:
boolean moved = mMoved;
// reset mMoved
mMoved = false;
boolean longClicked = mLongClicked;
mLongClicked = false;
if (mSlideAndSelect && moved){
event.setAction(MotionEvent.ACTION_CANCEL);
}
result = super.dispatchTouchEvent(event);
if (mSlideAndSelect){
mSlideAndSelect = false;
int upOffset = getOffset(x, y);
if (!moved && mDownOffset == upOffset && longClicked){
setSelection(mOldSelStart, mOldSelEnd);
showContextMenu();
}else{
setSelection(mDownOffset, upOffset);
}
return true;
}
return result;
case MotionEvent.ACTION_CANCEL:
mSlideAndSelect = false;
// reset mMoved
mMoved = false;
mLongClicked = false;
return super.dispatchTouchEvent(event);
default:
return super.dispatchTouchEvent(event);
}
}
protected void startSlideAndSelect() {
mSlideAndSelect = true;
ViewParent parent = getParent();
if (parent != null){
parent.requestDisallowInterceptTouchEvent(true);
}
}
private boolean mSlideAndSelect = false;
private boolean mLongClicked = false;
private Vibrator mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
#Override
public boolean onLongClick(View v) {
if (!mMoved){
startSlideAndSelect();
mLongClicked = true;
mVibrator.vibrate(30);
}
return true;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mSlideAndSelect){
mMagGlass.draw(canvas);
}
}
/**
* Need a drawable.mag_glass to work.
*
* #author lifurong
*
*/
class MagGlass{
private int mWidth, mHeight;
private Bitmap mMagGlassBitmap;
private int mX, mY;
private final static int INSET = 10;
public MagGlass(){
mMagGlassBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.mag_glass);
mWidth = mMagGlassBitmap.getWidth();
mHeight = mMagGlassBitmap.getHeight();
}
public void setObjectCenter(int x, int y){
mX = x;
mY = y;
}
public void draw(Canvas canvas) {
final float left = mX-mWidth/2.0f;
final float top = mY-mHeight/2.0f;
final float right = mX+mWidth/2.0f;
final float bottom = mY+mHeight/2.0f;
float vTrans = 80*mScale;
int vTransSign;
int[] location = new int[2];
getLocationInWindow(location);
int topEdge = location[1]-getPaddingTop()>0? 0:-location[1]+getPaddingTop();
if (top-vTrans > topEdge){
vTransSign = -1;
}else{
vTransSign = 1;
}
canvas.translate(0, vTrans*vTransSign);
canvas.clipRect(left, top, right, bottom);
canvas.drawBitmap(mMagGlassBitmap, left, top, null);
canvas.clipRect(left+INSET, top+INSET, right-INSET, bottom-INSET);
FastSelectEditText.super.onDraw(canvas);
}
}
}
To draw outside the bound of a view, you need to set the view's parent's clipChildren to false.
By default a ViewGroup has the clipChildrenset to true, which caused the children to draw on a canvas clipped to their bounds.