I'm trying to create a button-click animation, e.g. the button scales down a little when you press it, scales back up when you release. If you simply tap, you get the press and release strung together.
I set up an onTouchListener and a couple XML-defined AnimatorSets, one for the press and one for the release. Ran the press on ACTION_DOWN, the release on ACTION_UP or ACTION_CANCEL. This works fine when you press and hold the button, then release a little later. But with a quick tap, the release animation triggers before the press one is done, and often the result is no animation at all.
I'd hoped I could use AnimatorSet's sequential capabilities to stick the release animation onto the end of the possibly-already-running press animation, but no luck. I'm sure I could rig something up with callbacks, but that seems messy.
What's the best approach here? Thanks!
I built a sample Button class that buffers animations and executes them in a queue according to what you need:
public class AnimationButton extends Button
{
private List<Animation> mAnimationBuffer = new ArrayList<Animation>();;
private boolean mIsAnimating;
public AnimationButton(Context context)
{
super(context);
}
public AnimationButton(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public AnimationButton(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}
#Override
public boolean onTouchEvent(MotionEvent event)
{
if (event.getAction() == MotionEvent.ACTION_DOWN)
{
generateAnimation(1, 0.75f);
triggerNextAnimation();
}
else if (event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_UP)
{
generateAnimation(0.75f, 1);
triggerNextAnimation();
}
return super.onTouchEvent(event);
}
private void generateAnimation(float from, float to)
{
ScaleAnimation scaleAnimation = new ScaleAnimation(from, to, from, to);
scaleAnimation.setFillAfter(true);
scaleAnimation.setDuration(500);
scaleAnimation.setAnimationListener(new ScaleAnimation.AnimationListener()
{
#Override
public void onAnimationStart(Animation animation) { }
#Override
public void onAnimationRepeat(Animation animation) { }
#Override
public void onAnimationEnd(Animation animation)
{
mIsAnimating = false;
triggerNextAnimation();
}
});
mAnimationBuffer.add(scaleAnimation);
}
private void triggerNextAnimation()
{
if (mAnimationBuffer.size() > 0 && !mIsAnimating)
{
mIsAnimating = true;
Animation currAnimation = mAnimationBuffer.get(0);
mAnimationBuffer.remove(0);
startAnimation(currAnimation);
}
}
}
Hope this helps :)
Looks like L Preview adds the ability to specify animations for state changes in the xml file.
https://developer.android.com/preview/material/animations.html#viewstate
Related
I've mostly worked with iOS and have become accustomed to very smooth and fluent screen change animations. I am now working on an Android app and can't for the life of me get a fragment transaction to smoothly add/replace a fragment.
My set up is as follows: MainActivity has a FrameLayout for its xml which I immediately load FragmentA into on the MainActivity's OnCreate. My app then proceeds to replace the MainActivity's FrameLayout with FragmentB, FragmentC, ect. So my entire app has 1 activity.
On button clicks I call the following to add/replace the current fragment:
getFragmentManager()
.beginTransaction()
.setCustomAnimations(R.animator.slide_in, android.R.animator.fade_out, android.R.animator.fade_in, R.animator.slide_out)
.addToBackStack(null)
.add(R.id.fragment_1, fragment)
.commit();
slide_in.xml looks like (slide_out.xml is obviously the opposite):
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:interpolator="#android:interpolator/linear"
android:propertyName="xFraction"
android:valueType="floatType"
android:valueFrom="1.0"
android:valueTo="0"
android:duration="#android:integer/config_shortAnimTime"
/>
</set>
Sliding in and out I'm animating xFraction which I've subclassed LinearLayout to do like so:
public class SlidingLinearLayout extends LinearLayout {
private float yFraction = 0;
private float xFraction = 0;
public SlidingLinearLayout(Context context) {
super(context);
}
public SlidingLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private ViewTreeObserver.OnPreDrawListener preDrawListener = null;
public void setYFraction(float fraction) {
this.yFraction = fraction;
if (getHeight() == 0) {
if (preDrawListener == null) {
preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
setYFraction(yFraction);
return true;
}
};
getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}
return;
}
float translationY = getHeight() * fraction;
setTranslationY(translationY);
}
public float getYFraction() {
return this.yFraction;
}
public void setXFraction(float fraction) {
this.xFraction = fraction;
if (getWidth() == 0) {
if (preDrawListener == null) {
preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
setXFraction(xFraction);
return true;
}
};
getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}
return;
}
float translationX = getWidth() * fraction;
setTranslationX(translationX);
}
public float getxFraction() {
return xFraction;
}
}
So my issue is most of the time when I initially click a button to add/replace a fragment, I don't get an animation it just sort of pops into place. However more times than not when I press the back button and the fragment pops from the backstack the animation executes as expected. I feel like it is an issue with initialization while trying to animate. When the fragment is in memory and animated off screen it does so much better than when a new fragment is created then animated on to the screen. I'm curious if others have experienced this and if so how they resolved it.
This is an annoyance that has plagued my Android development experience that I'd really like to put to rest! Any help would be greatly appreciated. Thanks!
In my experience, if the fragment is doing too much work on load, then the system will basically skip the enter animation and just display the fragment. In my case the solution was to create an animation listener in onCreateAnimation to wait until the enter animation had finished before doing the process intensive work that was causing the animation to be skipped.
#Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if (nextAnim == 0) {
return super.onCreateAnimation(transit, enter, nextAnim);
}
Animation anim = android.view.animation.AnimationUtils.loadAnimation(getContext(), nextAnim);
anim.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {}
#Override
public void onAnimationEnd(Animation animation) {
// Do any process intensive work that can wait until after fragment has loaded
}
#Override
public void onAnimationRepeat(Animation animation) {}
});
return anim;
}
I have been racking my brains for days now on this and have Googled and SO'ed but can't seem to find anyone else reporting such problem(could have missed it).
I have a customized Mapview Class that listens for longpress among others and am using it to plot markers on my map. It plots okay on API-8. But on API-15 the marker is offsetted by about 2cm below where the user finger is doing the longpress. This is observed for both actual device (samsung s2) and eclipse emulator. Also the finger area long-pressed versus the marker area plotted(offset by about 2cm) is observed on all zoom levels.
Here is my customized Mapview Class (yanked it from somewhere):
public class MyCustomMapView extends MapView {
public interface OnLongpressListener {
public void onLongpress(MapView view, GeoPoint longpressLocation);
}
static final int LONGPRESS_THRESHOLD = 500;
private GeoPoint lastMapCenter;
private Timer longpressTimer = new Timer();
private MyCustomMapView.OnLongpressListener longpressListener;
public MyCustomMapView(Context context, String apiKey) {
super(context, apiKey);
}
public MyCustomMapView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyCustomMapView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setOnLongpressListener(MyCustomMapView.OnLongpressListener listener) {
longpressListener = listener;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
handleLongpress(event);
return super.onTouchEvent(event);
}
private void handleLongpress(final MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// Finger has touched screen.
longpressTimer = new Timer();
longpressTimer.schedule(new TimerTask() {
#Override
public void run() {
GeoPoint longpressLocation = getProjection().fromPixels((int)event.getX(), (int)event.getY());
/*
* Fire the listener. We pass the map location
* of the longpress as well, in case it is needed
* by the caller.
*/
longpressListener.onLongpress(MyCustomMapView.this, longpressLocation);
}
}, LONGPRESS_THRESHOLD);
lastMapCenter = getMapCenter();
}
if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (!getMapCenter().equals(lastMapCenter)) {
// User is panning the map, this is no longpress
longpressTimer.cancel();
}
lastMapCenter = getMapCenter();
}
if (event.getAction() == MotionEvent.ACTION_UP) {
// User has removed finger from map.
longpressTimer.cancel();
}
if (event.getPointerCount() > 1) {
// This is a multitouch event, probably zooming.
longpressTimer.cancel();
}
}
And this is how I call the Class above:
custom_marker = getResources().getDrawable(R.drawable.marker3);
custom_marker.setBounds(-custom_marker.getIntrinsicWidth(), -custom_marker.getIntrinsicHeight(), 0, 0);
customSitesOverlay = new CustomSitesOverlay(custom_marker);
mapView.getOverlays().add(customSitesOverlay);
customSitesOverlay.addOverlay(new OverlayItem(longpressLocation, "User Marker", id));
It looks like a setBounds problem. You are setting the marker bounds to center in the bottom-right corner of drawable. Is that the intented behaviour?
You can set the marker to center in the bottom-center (which is the most usual type or markers) setting the bounds to:
custom_marker.setBounds(-custom_marker.getIntrinsicWidth()/2, -custom_marker.getIntrinsicHeight(), custom_marker.getIntrinsicWidth()/2, 0);
good luck.
I made a custom background for a button and also for different button states. But now I made to a point that I cannot understand.
When button is in normal state then it looks just fine. But when I press the button, I need to move text down few pixels because button background image moving (actually it feels like it moving on the image, because first there's border under the button and when it's in pressed state then this border disappears). Please see image below.
How can I move the buttons text in the button when buttons state is pressed? (maybe padding somehow or layout custom for a button)
Setting padding in 9-patch didn't work for me.
Setting padding in touch listeners is messy, would litter code anywhere buttons are used.
I went with subclassing Button, and it turned out reasonably tidy. In my case, I wanted to offset icon (drawableLeft) and text 1px left and 1px down.
Subclassed button widget:
package com.myapp.widgets;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.Button;
import com.myapp.R;
public class OffsetButton extends Button {
public OffsetButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public OffsetButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public OffsetButton(Context context) {
super(context);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
boolean value = super.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP) {
setBackgroundResource(R.drawable.btn_normal);
} else if (event.getAction() == MotionEvent.ACTION_DOWN) {
setBackgroundResource(R.drawable.btn_pressed);
setPadding(getPaddingLeft() + 1, getPaddingTop() + 1, getPaddingRight() - 1,
getPaddingBottom() - 1);
}
return value;
}
}
And use it in layout like this:
<com.myapp.widgets.OffsetButton
android:text="#string/click_me"
android:drawableLeft="#drawable/icon_will_be_offset_too_thats_good" />
Few notes:
I did not use StateListDrawable for background, and am instead switching backgrounds in code. When I tried using StateListDrawable, there would be small pause between padding change and background change. That didn't look good.
Setting background resets padding, so don't need to adjust padding in ACTION_UP case
It was important to increase top and left padding, and at the same time decrease bottom and right padding. So the size of content area stays the same and content area is effectively just shifted.
I did not try it myself but if you use nine-patch as a background drawable for both states then you should consider setting proper padding box in pressed state drawable. See details here.
You can use padding on the view. You can add an OnTouchListener to the button or view like
viewF.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction == MotionEvent.ACTION_UP) {
//set your padding
} else if (event.getAction == MotionEvent.ACTION_DOWN){
//set your padding
}
return true;
}
});
The ontouchlistener will let your know when the button is pressed and not.
The PÄ“teris Caune answer works worse than overriding setPressed(). But everithing OK with setPressed until you test your app at ICS or lower device and add button as listview item.
To archieve this I've improved my button's class:
public class OffsetButton extends Button {
private static final int OFFSET_IN_DP = 6;
private int offset_in_px;
private boolean wasPressed = false;
private Integer[] defaultPaddings;
public OffsetButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
public OffsetButton(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public OffsetButton(Context context) {
super(context);
initView();
}
private void initView() {
offset_in_px = (int) DisplayUtil.convertDpToPixel(OFFSET_IN_DP);
}
#Override
public void setPressed(boolean pressed) {
if (pressed && !wasPressed) {
changePaddings();
}
if (!pressed && wasPressed) {
resetPaddings();
}
super.setPressed(pressed);
}
private void changePaddings() {
defaultPaddings = new Integer[]{getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()};
setPadding(getPaddingLeft(), getPaddingTop() + offset_in_px, getPaddingRight(), getPaddingBottom() - offset_in_px);
wasPressed = true;
}
private void resetPaddings() {
setPadding(defaultPaddings[0], defaultPaddings[1], defaultPaddings[2], defaultPaddings[3]);
wasPressed = false;
}
#Override
public boolean performClick() {
resetPaddings();
return super.performClick();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (isEnabled())
{
if (event.getAction() == MotionEvent.ACTION_DOWN && !wasPressed) {
changePaddings();
} else if (event.getAction() == MotionEvent.ACTION_UP && wasPressed) {
resetPaddings();
}
}
return super.onTouchEvent(event);
}
}
this might just work:
setPadding(left, top, right, bottom); // Normal
setPadding(left, top + x, right, bottom - x); // Pressed
Just an interesting query here, is there a way to capture when a zoom animation sequence has ended when calling either:
MapController.zoomIn() or MapController.zoomOut();
I know that it does kick off an animation sequence to zoom in/out to the next level, however there is no known way I can find/google search, etc to find out when it finishes that sequence. I need to be able to run an update command when that is stopped so my map updates correctly.
I've found that by running the update command after calling the above function the Projection isn't from the zoom out level but somewhere inbetween (so I can't show all the data I need).
I have to admit I punted here, it's a hack but it works great. I started off with a need to know when a zoom occured, and once I hooked into that (and after some interesting debugging) I found some values were "between zoom" values, so I needed to wait till after the zoom was done.
As suggested elsewhere on Stack Overflow my zoom listener is an overridden MapView.dispatchDraw that checks to see if the zoom level has changed since last time.
Beyond that I added an isResizing method that checks if the timestamp is more than 100ms since the getLongitudeSpan value stopped changing. Works great. Here is the code:
My very first Stack Overflow post! Whoo Hoo!
public class MapViewWithZoomListener extends MapView {
private int oldZoomLevel = -1;
private List<OnClickListener> listeners = new ArrayList<OnClickListener>();
private long resizingLongitudeSpan = getLongitudeSpan();
private long resizingTime = new Date().getTime();
public MapViewWithZoomListener(Context context, String s) {
super(context, s);
}
public MapViewWithZoomListener(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}
public MapViewWithZoomListener(Context context, AttributeSet attributeSet, int i) {
super(context, attributeSet, i);
}
public boolean isResizing() {
// done resizing if 100ms has elapsed without a change in getLongitudeSpan
return (new Date().getTime() - resizingTime < 100);
}
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (getZoomLevel() != oldZoomLevel) {
new AsyncTask() {
#Override
protected Object doInBackground(Object... objects) {
try {
if (getLongitudeSpan() != resizingLongitudeSpan) {
resizingLongitudeSpan = getLongitudeSpan();
resizingTime = new Date().getTime();
}
Thread.sleep(125); //slightly larger than isMoving threshold
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
#Override
protected void onPostExecute(Object o) {
super.onPostExecute(o);
if (!isResizing() && oldZoomLevel != getZoomLevel()) {
oldZoomLevel = getZoomLevel();
invalidate();
for (OnClickListener listener : listeners) {
listener.onClick(null);
}
}
}
}.execute();
}
}
public void addZoomListener(OnClickListener listener) {
listeners.add(listener);
}
I would like to have onZoomListener on my MapView.
The code below is what I have done. It registers if zoom buttons are tapped. Since all new phones now supports pinch to zoom, this is useless. Does anybody have idea how to do real onZoomListener? Thanks.
OnZoomListener listener = new OnZoomListener() {
#Override
public void onVisibilityChanged(boolean arg0) {
// TODO Auto-generated method stub
}
#Override
public void onZoom(boolean arg0) {
Log.d(TAG, "ZOOM CHANGED");
// TODO Auto-generated method stub
}
};
ZoomButtonsController zoomButton = mapView.getZoomButtonsController();
zoomButton.setOnZoomListener(listener);
I had to subclass MapView and override dispatchDraw
Here is the code:
int oldZoomLevel=-1;
#Override
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (getZoomLevel() != oldZoomLevel) {
Log.d(TAG, "ZOOOMED");
oldZoomLevel = getZoomLevel();
}
}
This blog helped me a lot: http://pa.rezendi.com/2010/03/responding-to-zooms-and-pans-in.html
Above works great. Is there maybe simpler solution?
I tried to implement onTouchListener on MapView directly but touch event would be detected only once if onTouchListener would return false. If it would return true, touch would be detected every time, but map zooming and panning wouldn't work.
I put all the code from above together and came up with this class:
package at.blockhaus.wheels.map;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
public class CustomMapView extends MapView {
private int oldZoomLevel = -1;
private GeoPoint oldCenterGeoPoint;
private OnPanAndZoomListener mListener;
public CustomMapView(Context context, String apiKey) {
super(context, apiKey);
}
public CustomMapView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomMapView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setOnPanListener(OnPanAndZoomListener listener) {
mListener = listener;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
GeoPoint centerGeoPoint = this.getMapCenter();
if (oldCenterGeoPoint == null ||
(oldCenterGeoPoint.getLatitudeE6() != centerGeoPoint.getLatitudeE6()) ||
(oldCenterGeoPoint.getLongitudeE6() != centerGeoPoint.getLongitudeE6()) ) {
mListener.onPan();
}
oldCenterGeoPoint = this.getMapCenter();
}
return super.onTouchEvent(ev);
}
#Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (getZoomLevel() != oldZoomLevel) {
mListener.onZoom();
oldZoomLevel = getZoomLevel();
}
}
}
And the corresponding listener interface:
package at.blockhaus.wheels.map;
public interface OnPanAndZoomListener {
public void onPan();
public void onZoom();
}
Maybe a bit late, but have you tried with an OnOverlayGestureListener?
http://code.google.com/p/mapview-overlay-manager/wiki/OnOverlayGestureListener
Try via this method
map.setMapListener(new MapListener()
{
....
#Override
public boolean onZoom(ZoomEvent event)
{
return false;
}
});
Edit: I believe what you've found is probably the only mechanism to allow this detection. (Editing my post to remove misinformation) Extending the MapView and overriding the onDraw() method would work. One consideration to make will be how often to fire the code listening to zoom. For instance, while zooming the onDraw may be called dozens of times each with a different zoom level. This may cause poor performance if your listener is firing every time. You could throttle this by determining that a zoom change has taken place and then waiting for the zoom level to be the same on two subsequent redraws before firing the event. This all depends on how you want your code to be called.
use dispatchTouch() method to detect touch events on the map and make sure you call the super() method to carry out the default map touch functions. (Setting onTouchListener will disable the in built events like zooming and panning if you return true from your functiion, and will handle touch only once if you return false.)
in dispatchTouch method, you can check for no of touch points by using event.getPointerCount(). use Action_UP to detect whether the user has zoomed out or not by calling mapview.getZoomLevel(); if you find the zoom level changed, you can carry out your actions.
i implemented it the following way, and it works for single/multitouch :
#Override
protected void dispatchDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.dispatchDraw(canvas);
if (oldZoomLevel == -1) {
oldZoomLevel = getZoomLevel();
}
if (oldZoomLevel < getZoomLevel()) {
System.out.println("zoomed in");
if (mOnZoomInListener != null) {
mOnZoomInListener
.onZoomedIn(this, oldZoomLevel, getZoomLevel());
}
}
if (oldZoomLevel > getZoomLevel()) {
System.out.println("zoomed out");
if (mOnZoomOutListener != null) {
mOnZoomOutListener.onZoomedOut(this, oldZoomLevel,
getZoomLevel());
}
}
It worked for me!
MapView provides build-in zoom controls that support pinch.
If it doesn't work out of the box you could try setting mapView.setBuiltInZoomControls(true);
This way you should not need the OnZoomListener.