I want to create a rotating progress image, and wonder what's the best way to proceed. I can make it work with an animation list with for example 12 images changing every 100ms. This works fine, but it's quite tedious to create 12 images or for every size and resolution:
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false">
<item android:drawable="#drawable/ic_loading_grey_on_black_01" android:duration="100" />
<item android:drawable="#drawable/ic_loading_grey_on_black_02" android:duration="100" />
<item android:drawable="#drawable/ic_loading_grey_on_black_03" android:duration="100" />
<item android:drawable="#drawable/ic_loading_grey_on_black_04" android:duration="100" />
<item android:drawable="#drawable/ic_loading_grey_on_black_05" android:duration="100" />
<item android:drawable="#drawable/ic_loading_grey_on_black_06" android:duration="100" />
<item android:drawable="#drawable/ic_loading_grey_on_black_07" android:duration="100" />
<item android:drawable="#drawable/ic_loading_grey_on_black_08" android:duration="100" />
<item android:drawable="#drawable/ic_loading_grey_on_black_09" android:duration="100" />
<item android:drawable="#drawable/ic_loading_grey_on_black_10" android:duration="100" />
<item android:drawable="#drawable/ic_loading_grey_on_black_11" android:duration="100" />
<item android:drawable="#drawable/ic_loading_grey_on_black_12" android:duration="100" />
I suppose that an easier solution is to use one image per resolution, but rather rotate it for each frame. In the platform resources (android-sdk-windows/platforms...) I found something called animated-rotate in the file drawable/search_spinner.xml, but if I copy the code get a compiler error complaining about android:framesCount and android:frameDuration (Google APIs 2.2 in Eclipse):
<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="#drawable/spinner_black_20"
android:pivotX="50%"
android:pivotY="50%"
android:framesCount="12"
android:frameDuration="100" />
I have also tried using a repeating rotate animation (using in the anim resource folder), but I actually prefer the look of the animation list version.
What is the recommended way of solving this problem?
Rotate drawable suggested by Praveen won't give you control of frame count. Let's assume you want to implement a custom loader which consists from 8 sections:
Using animation-list approach, you need to create 8 frames rotated by 45*frameNumber degrees manually. Alternatively, you can use 1st frame and set rotation animation to it:
File res/anim/progress_anim.xml:
<?xml version="1.0" encoding="utf-8"?>
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="infinite" />
File MainActivity.java
Animation a = AnimationUtils.loadAnimation(getContext(), R.anim.progress_anim);
a.setDuration(1000);
imageView.startAnimation(a);
This will give you smooth animation instead of 8-stepped. To fix this we need to implement custom interpolator:
a.setInterpolator(new Interpolator() {
private final int frameCount = 8;
#Override
public float getInterpolation(float input) {
return (float)Math.floor(input*frameCount)/frameCount;
}
});
Also you can create a custom widget:
File res/values/attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ProgressView">
<attr name="frameCount" format="integer"/>
<attr name="duration" format="integer" />
</declare-styleable>
</resources>
File ProgressView.java:
public class ProgressView extends ImageView {
public ProgressView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setAnimation(attrs);
}
public ProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
setAnimation(attrs);
}
public ProgressView(Context context) {
super(context);
}
private void setAnimation(AttributeSet attrs) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ProgressView);
int frameCount = a.getInt(R.styleable.ProgressView_frameCount, 12);
int duration = a.getInt(R.styleable.ProgressView_duration, 1000);
a.recycle();
setAnimation(frameCount, duration);
}
public void setAnimation(final int frameCount, final int duration) {
Animation a = AnimationUtils.loadAnimation(getContext(), R.anim.progress_anim);
a.setDuration(duration);
a.setInterpolator(new Interpolator() {
#Override
public float getInterpolation(float input) {
return (float)Math.floor(input*frameCount)/frameCount;
}
});
startAnimation(a);
}
}
File activity_main.xml:
<com.example.widget.ProgressView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_progress"
app:frameCount="8"
app:duration="1000"/>
File res/anim/progress_anim.xml: listed above
You have to create a drawable xml file like below:
Code:
<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:pivotX="50%" android:pivotY="50%" android:fromDegrees="0"
android:toDegrees="360" android:drawable="#drawable/imagefile_to_rotate" />
I found vokilam's answer to be the best one to create a nice stepped/staggered animation. I went for his final suggestion and made a custom widget, the only problem I encountered was that setting visibility wouldn't work because it was animated and thus would always be visible...
I adjusted his code (ProgressView.java which I renamed StaggeredProgress.java) like this:
public class StaggeredProgress extends ImageView {
private Animation staggered;
public StaggeredProgress(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setAnimation(attrs);
}
public StaggeredProgress(Context context, AttributeSet attrs) {
super(context, attrs);
setAnimation(attrs);
}
public StaggeredProgress(Context context) {
super(context);
}
private void setAnimation(AttributeSet attrs) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.StaggeredProgress);
int frameCount = a.getInt(R.styleable.StaggeredProgress_frameCount, 12);
int duration = a.getInt(R.styleable.StaggeredProgress_duration, 1000);
a.recycle();
setAnimation(frameCount, duration);
}
public void setAnimation(final int frameCount, final int duration) {
Animation a = AnimationUtils.loadAnimation(getContext(), R.anim.progress_anim);
a.setDuration(duration);
a.setInterpolator(new Interpolator() {
#Override
public float getInterpolation(float input) {
return (float)Math.floor(input*frameCount)/frameCount;
}
});
staggered = a;
//startAnimation(a);
}
#Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
if( visibility == View.VISIBLE )
startAnimation(staggered);
else
clearAnimation();
}
}
This way setting the view's visibility starts and stops the animation as required...Many thanks again to vokilam!
see examples here
http://developer.android.com/resources/samples/ApiDemos/src/com/example/android/apis/view/index.html
specifically:
Progress Bar
Incremental
Demonstrates large and small rotating progress indicators that can be incremented or decremented in units.
Smooth
Demonstrates large and small continuously rotating progress indicators used to indicate a generic "busy" message.
Dialogs
Demonstrates a ProgressDialog, a popup dialog that hosts a progress bar. This example demonstrates both determinate and indeterminate progress indicators.
In Title Bar
Demonstrates an Activity screen with a progress indicator loaded by setting the WindowPolicy's progress indicator feature.
SACPK's solution definitely works. Another solution can be to use <animated-rotate> just like in question and remove android:framesCount="12"
android:frameDuration="100" attributes for those the compiler complains. It still works even for my 8-frame image.
However, I havn't figured out how to control the speed of the animation :(.
Thank #vokilam. This similar solution (a custom view that rotates automatically) uses <animation-list> dynamically in its implementation:
public class FramesAnimatorView extends AppCompatImageView {
private int framesCount;
private int duration;
private Bitmap frameBitmap;
public FramesAnimatorView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
public FramesAnimatorView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public FramesAnimatorView(Context context) { super(context); }
private void init(Context context, AttributeSet attrs) {
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FramesAnimatorView);
framesCount = typedArray.getInt(R.styleable.FramesAnimatorView_framesCount, 12);
duration = typedArray.getInt(R.styleable.FramesAnimatorView_duration, 1200);
typedArray.recycle();
// Method 1: Use <rotate> as Animation (RotateAnimation) and startAnimation() (Rotate view itself).
//method1(framesCount, duration);
// Method 2: Use <rotate> as Drawable (RotateDrawable) and ObjectAnimator. Usable for API 21+ (because of using RotateDrawable.setDrawable).
//method2();
// Method 3 (Recommended): Use <animation-list> (AnimationDrawable) dynamically.
final int frameDuration = this.duration / framesCount;
final AnimationDrawable animationDrawable = (AnimationDrawable) getDrawable();
for (int i = 0; i < framesCount; i++)
animationDrawable.addFrame(
new RotatedDrawable(frameBitmap, i * 360f / framesCount, getResources()),
frameDuration);
animationDrawable.start();
}
#Override public void setImageResource(int resId) { //info();
frameBitmap = BitmapFactory.decodeResource(getResources(), resId);
super.setImageDrawable(new AnimationDrawable());
}
#Override public void setImageDrawable(#Nullable Drawable drawable) { //info();
frameBitmap = drawableToBitmap(drawable);
super.setImageDrawable(new AnimationDrawable());
}
#Override public void setImageBitmap(Bitmap bitmap) { //info();
frameBitmap = bitmap;
super.setImageDrawable(new AnimationDrawable());
}
/**
* See #android-developer's answer on stackoverflow.com.
*/
private static class RotatedDrawable extends BitmapDrawable {
private final float degrees;
private int pivotX;
private int pivotY;
RotatedDrawable(Bitmap bitmap, float degrees, Resources res) {
super(res, bitmap);
pivotX = bitmap.getWidth() / 2;
pivotY = bitmap.getHeight() / 2;
this.degrees = degrees;
}
#Override public void draw(final Canvas canvas) {
canvas.save();
canvas.rotate(degrees, pivotX, pivotY);
super.draw(canvas);
canvas.restore();
}
}
/**
* See #André's answer on stackoverflow.com.
*/
#NonNull private static Bitmap drawableToBitmap(Drawable drawable) {
final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}
See Android-FramesAnimatorView on GitHub for full (and probably more updated) source code.
Related
I want to display this animation in an Android app:
So I created a <animation-list> to do so. It uses 46 frames, each one consisting of a 200x200px png:
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="#drawable/anim_loading_ripple_frame_0" android:duration="45" />
<item android:drawable="#drawable/anim_loading_ripple_frame_1" android:duration="45" />
<item android:drawable="#drawable/anim_loading_ripple_frame_2" android:duration="45" />
<item android:drawable="#drawable/anim_loading_ripple_frame_3" android:duration="45" />
<item android:drawable="#drawable/anim_loading_ripple_frame_4" android:duration="45" />
<item android:drawable="#drawable/anim_loading_ripple_frame_5" android:duration="45" />
...
<item android:drawable="#drawable/anim_loading_ripple_frame_45" android:duration="45" />
</animation-list>
Then I set this as the background of an ImageView and start the animation using
AnimationDrawable loadingAnimation =
(AnimationDrawable) loadingAnimationView.getBackground();
loadingAnimation.start();
Everything works perfect, except for the fact that the app sometimes crashes due to an OutOfMemoryException! It seems that the memory requirements by this kind of animation are quite high, as I've read others experiencing the same issues.
Then I thought I could probably just make each frame into an XML shape drawable, so I tried experimenting, but I can't figure out how to make the circles change size - they always occupy the whole width of the view.
I tried doing something like this
<item>
<shape android:shape="oval">
<padding android:top="30dp" android:right="30dp" android:bottom="30dp" android:left="30dp" />
<size
android:width="100dp"
android:height="100dp" />
<stroke
android:color="#0F0"
android:width="4dp"/>
</shape>
</item>
And also using a layer-list to define another shape in the same XML file but with a different size, thinking that would cause the circle to become smaller, but they both ended up occupying 100% of the view.
Also, I'm not sure if the AnimationDrawable will actually use less memory if I define the frames in XML as opposed to a series of png images? Perhaps I'll still run into the same memory usage issue?
What alternatives do I have? If time wasn't an issue, I'd try to work out a custom View, but it seems like a lot of work just for this animation.
The ScaleAnimation solution provided by Ves will work, but I figured I might as well dive into it and make a proper (and I guess quite more efficient) custom View. Especially when dealing with multiple "ripples".
This is the entire View class, tweaks to the ripples can be made in init() to change colors, number of ripples, etc.
RippleView.java
public class RippleView extends View implements ValueAnimator.AnimatorUpdateListener {
public static final String TAG = "RippleView";
private class Ripple {
AnimatorSet mAnimatorSet;
ValueAnimator mRadiusAnimator;
ValueAnimator mAlphaAnimator;
Paint mPaint;
Ripple(float startRadiusFraction, float stopRadiusFraction, float startAlpha, float stopAlpha, int color, long delay, long duration, float strokeWidth, ValueAnimator.AnimatorUpdateListener updateListener){
mRadiusAnimator = ValueAnimator.ofFloat(startRadiusFraction, stopRadiusFraction);
mRadiusAnimator.setDuration(duration);
mRadiusAnimator.setRepeatCount(ValueAnimator.INFINITE);
mRadiusAnimator.addUpdateListener(updateListener);
mRadiusAnimator.setInterpolator(new DecelerateInterpolator());
mAlphaAnimator = ValueAnimator.ofFloat(startAlpha, stopAlpha);
mAlphaAnimator.setDuration(duration);
mAlphaAnimator.setRepeatCount(ValueAnimator.INFINITE);
mAlphaAnimator.addUpdateListener(updateListener);
mAlphaAnimator.setInterpolator(new DecelerateInterpolator());
mAnimatorSet = new AnimatorSet();
mAnimatorSet.playTogether(mRadiusAnimator, mAlphaAnimator);
mAnimatorSet.setStartDelay(delay);
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(color);
mPaint.setAlpha((int)(255*startAlpha));
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(strokeWidth);
}
void draw(Canvas canvas, int centerX, int centerY, float radiusMultiplicator){
mPaint.setAlpha( (int)(255*(float)mAlphaAnimator.getAnimatedValue()) );
canvas.drawCircle(centerX, centerY, (float)mRadiusAnimator.getAnimatedValue()*radiusMultiplicator, mPaint);
}
void startAnimation(){
mAnimatorSet.start();
}
void stopAnimation(){
mAnimatorSet.cancel();
}
}
private List<Ripple> mRipples = new ArrayList<>();
public RippleView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public RippleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public RippleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
if( isInEditMode() )
return;
/*
Tweak your ripples here!
*/
mRipples = new ArrayList<>();
mRipples.add(new Ripple(0.0f, 1.0f, 1.0f, 0.0f, Color.RED, 0, 2000, 4, this));
mRipples.add(new Ripple(0.0f, 1.0f, 1.0f, 0.0f, Color.WHITE, 500, 2000, 4, this));
}
public void startAnimation() {
setVisibility(View.VISIBLE);
for (Ripple ripple : mRipples) {
ripple.startAnimation();
}
}
public void stopAnimation() {
for (Ripple ripple : mRipples) {
ripple.stopAnimation();
}
setVisibility(View.GONE);
}
#Override
public void onAnimationUpdate(ValueAnimator animation) {
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
int centerX = getWidth()/2;
int centerY = getHeight()/2;
int radiusMultiplicator = getWidth()/2;
for (Ripple ripple : mRipples) {
ripple.draw(canvas, centerX, centerY, radiusMultiplicator);
}
}
}
Just call .startAnimation() on the RippleView to start the animation.
RippleView r = findViewById(R.id.rippleView);
r.startAnimation();
You can use ScaleAnimation to change your size. I created an ImageView and used the shape you defined above:
<ImageView
android:id="#+id/circle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/circle"/>
It is slightly different than you wanted because I did not bother to find the center point to set the animation. It is scaling from the upper left corner. (0,0)
ImageView iv = findViewById(R.id.circle);
float startScale = 0.5f;
float endScale = 3.0f;
Animation scaleAnim = new ScaleAnimation( startScale, endScale,
startScale, endScale,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 0);
scaleAnim.setRepeatCount(Animation.INFINITE);
scaleAnim.setRepeatMode(Animation.REPEAT);
scaleAnim.setDuration(2000);
iv.setAnimation(scaleAnim);
scaleAnim.start();
You can similarly animation the color using a ValueAnimation.
So I am running into a weird issue... I have made some code to tint a Drawable, and it is working on all Android versions for Vector assets, but not for regular PNG assets. The code is below:
public class TintHelper {
private Context mContext;
public TintHelper(Context context) {
mContext = context;
}
public Drawable getTintedDrawableFromResource(int resourceID, ColorStateList colorStateList) {
Drawable original = AppCompatDrawableManager.get().getDrawable(mContext, resourceID);
return performTintOnDrawable(original, colorStateList);
}
private Drawable performTintOnDrawable(Drawable drawable, ColorStateList colorStateList) {
Drawable tinted = DrawableCompat.wrap(drawable);
DrawableCompat.setTintList(tinted, colorStateList);
return tinted;
}
}
When I specify the resource ID of a vector asset, the code works perfectly and the image is tinted when pressed, but when I use a regular PNG, there is no tint applied when the icon is pressed. If anyone has any ideas of why this doesn't work, please post an alternative method that could potentially support both asset types.
Thanks in advance!
It is work for PNG in my environment.
set like this:
int resourceID = R.drawable.ic_launcher;
TintHelper tintHelper = new TintHelper(this);
Drawable drawable = tintHelper.getTintedDrawableFromResource(resourceID,
ContextCompat.getColorStateList(this, R.color.colors));
ImageView imageView = (ImageView) findViewById(R.id.image);
imageView.setImageDrawable(drawable);
colors.xml is like this:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true" android:color="#android:color/holo_red_dark"/>
<item android:state_selected="true" android:color="#android:color/holo_red_dark"/>
<item android:state_pressed="true" android:color="#android:color/holo_red_dark"/>
<item android:color="#android:color/white"/>
</selector>
I found the problem. Essentially, DrawableCompat.setTintList() is not working as expected on Android 21 and above. This is due to their implementation not calling invalidate() when there are state changes. More details can be read on this bug report.
To get this tinting code working for all platforms and all resource types, I needed to create a custom ImageView class as shown below:
public class StyleableImageView extends AppCompatImageView {
public StyleableImageView(Context context) {
super(context);
}
public StyleableImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public StyleableImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// This is the function to override...
#Override
protected void drawableStateChanged() {
super.drawableStateChanged();
invalidate(); // THE IMPORTANT LINE
}
}
Hopefully this helps someone that has had to deal with a similar situation.
I'm trying to add custom transition to fragments. As the Link suggested , the proper solution is to create a custom view as fragment container and then by animating the new added property , make fragment's transition run. but absolutely its on java. I implemented that as below in C# and Xamarin:
class SmartFrameLayout : FrameLayout
{
public SmartFrameLayout(Context context) : base(context) { }
public SmartFrameLayout(Context context, IAttributeSet attrs) : base(context, attrs) { }
public SmartFrameLayout(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr) { }
public SmartFrameLayout(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes) { }
//public float getXFraction()
//{
// if (Width == 0) return 0;
// return GetX() / Width;
//}
//public void setXFraction(float fraction)
//{
// Log.Debug("Fraction", fraction.ToString());
// float xx = GetX();
// SetX(xx * fraction);
//}
//private float XFraction;
public float XFraction
{
get {
if (Width == 0) return 0;
return GetX() / Width;
}
set {
float xx = GetX();
SetX(xx * value);
}
}
}
As you can see, first I tried to implement that same as the tutorial (Except that c# doesn't support read-only local variable as a "final" replacement!)
but in objectAnimator the property did not called properly. Then I think maybe using C# property will solve the problem. But it didn't.
Here is my animation xml file, named "from_right.xml":
<?xml version="1.0" encoding="utf-8" ?>
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="#android:anim/accelerate_decelerate_interpolator"
android:propertyName="xFraction"
android:valueType="floatType"
android:valueFrom="1.0"
android:valueTo="0"
android:duration="500"/>
I changed propertyName to "XFraction" or even anything else, but the results was the same.
Using "x" as propertyName and "1000" as valueFrom works well.
So I figured out the main problem is that the objectAnimator could not call setXFraction at all!
Please tell me what I'm doing wrong, or if there is a better solution to get exactly the screen width for valueFrom in objectAnimator !
You need to expose the setXFraction and getXFraction methods to Java; they are currently only within managed code and are not accesible to the Java VM.
Use the [Export] attribute to expose these methods to Java so that the animator can use them:
[Export]
public float getXFraction()
{
if (Width == 0) return 0;
return GetX() / Width;
}
[Export]
public void setXFraction(float fraction)
{
Log.Debug("Fraction", fraction.ToString());
float xx = GetX();
SetX(xx * fraction);
}
This will result in the following Java code being generated in SmartFrameLayouts Android Callable Wrapper:
public float getXFraction ()
{
return n_getXFraction ();
}
private native float n_getXFraction ();
public void setXFraction (float p0)
{
n_setXFraction (p0);
}
private native void n_setXFraction (float p0);
I wrote a suduko game for android, and want to animate tile, if the user insert wrong number.
But I don't figure how to do this? The problem is that, I have one big rectangel wich cover hole screen, and devide this rectangle in tiles, simply by drawnig lines. And now I can't figure how to animate tiles.
Hop I could explain my problem.
Sorry for bad english, and thaks for answers)
You should not draw your board like that. I would recommend to implement one tile as a subclass of View so you can then animate each one individually.
Try this to see how animation works:
Tile class
public class Tile extends View {
private RectF mRect;
private Paint mPaint;
public Tile(Context context) {
super(context);
init();
}
public Tile(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public Tile(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init(){
mRect = new RectF(0, 0, 100, 100);
mPaint = new Paint();
mPaint.setStyle( Paint.Style.STROKE );
mPaint.setColor( Color.BLUE );
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(mRect, mPaint);
}
}
Main activity
public class MainActivity extends Activity implements OnClickListener {
private Tile mTile;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout layout = new LinearLayout( this );
layout.setBackgroundColor( Color.WHITE );
layout.setPadding(50, 50, 50, 50);
Button btn = new Button( this );
btn.setText( "Click Me" );
btn.setOnClickListener( this );
layout.addView( btn );
mTile = new Tile( this );
layout.addView( mTile );
setContentView( layout );
}
#Override
public void onClick(View v) {
Animation scaleAnim = AnimationUtils.loadAnimation(this, R.anim.scale);
mTile.startAnimation( scaleAnim );
}
}
Animation definition (This file should be named scale.xml and placed under directory /res/anim )
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<scale
android:interpolator="#android:anim/accelerate_decelerate_interpolator"
android:fromXScale="1.0"
android:toXScale="2.0"
android:fromYScale="1.0"
android:toYScale="2.0"
android:fillAfter="false"
android:repeatCount="1"
android:repeatMode="reverse"
android:duration="700" />
</set>
Learn more about animations here.
To make your own animation have a look here here.
Hope this keeps you going.
If you use standard views, you could look into tween animation, i.e. define a set of animations, load them from the resources and attach them to your views / start the animation.
Have a look at the spaceship jump example here.
If you do custom drawing, I'm afraid you also need to do custom animation.
Working with animation on anything before Honeycomb is a real pain. I recommend downloading the NineOldAndroids library (http://nineoldandroids.com/) and using it as imports, that way you can use the Honeycomb animation API on all versions of android (even up to 1.0).
Is it possible to create a drawable that has some sort of animation, whether it is a frame by frame animation, rotation, etc, that is defined as a xml drawable and can be represented by a single Drawable object without having to deal with the animation in code?
How I am thinking to use it:
I have a list and each item in this list may at sometime have something happening to it. While it is happening, I would like to have a spinning progress animation similar to a indeterminate ProgressBar. Since there may also be several of these on screen I thought that if they all shared the same Drawable they would only need one instance of it in memory and their animations would be synced so you wouldn't have a bunch of spinning objects in various points in the spinning animation.
I'm not attached to this approach. I'm just trying to think of the most efficient way to display several spinning progress animations and ideally have them synced together so they are consistent in appearance.
Thanks
In response to Sybiam's answer:
I have tried implementing a RotateDrawable but it is not rotating.
Here is my xml for the drawable so far:
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="#drawable/my_drawable_to_rotate"
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
android:duration="800"
android:visible="true" />
I have tried using that drawable as the src and background of a ImageView and both ways only produced a non-rotating image.
Is there something that has to start the image rotation?
Yes! The (undocumented) key, which I discovered by reading the ProgressBar code is that you have to call Drawable.setLevel() in onDraw() in order for the <rotate> thing to have any effect. The ProgressBar works something like this (extra unimportant code omitted):
The drawable XML:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<rotate
android:drawable="#drawable/spinner_48_outer_holo"
android:pivotX="50%"
android:pivotY="50%"
android:fromDegrees="0"
android:toDegrees="1080" />
</item>
<item>
<rotate
android:drawable="#drawable/spinner_48_inner_holo"
android:pivotX="50%"
android:pivotY="50%"
android:fromDegrees="720"
android:toDegrees="0" />
</item>
</layer-list>
In onDraw():
Drawable d = getDrawable();
if (d != null)
{
// Translate canvas so a indeterminate circular progress bar with
// padding rotates properly in its animation
canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop());
long time = getDrawingTime();
// I'm not sure about the +1.
float prog = (float)(time % ANIM_PERIOD+1) / (float)ANIM_PERIOD;
int level = (int)(MAX_LEVEL * prog);
d.setLevel(level);
d.draw(canvas);
canvas.restore();
ViewCompat.postInvalidateOnAnimation(this);
}
MAX_LEVEL is a constant, and is always 10000 (according to the docs).
ANIM_PERIOD is the period of your animation in milliseconds.
Unfortunately since you need to modify onDraw() you can't just put this drawable in an ImageView since ImageView never changes the drawable level. However you may be able to change the drawable level from outside the ImageView's. ProgressBar (ab)uses an AlphaAnimation to set the level. So you'd do something like this:
mMyImageView.setImageDrawable(myDrawable);
ObjectAnimator anim = ObjectAnimator.ofInt(myDrawable, "level", 0, MAX_LEVEL);
anim.setRepeatCount(ObjectAnimator.INFINITE);
anim.start();
It might work but I haven't tested it.
Edit
There is actually an ImageView.setImageLevel() method so it might be as simple as:
ObjectAnimator anim = ObjectAnimator.ofInt(myImageVew, "ImageLevel", 0, MAX_LEVEL);
anim.setRepeatCount(ObjectAnimator.INFINITE);
anim.start();
Drawables
There you go! And this one for RotateDrawable. I believe that from the Doc it should be pretty straitght forward. You can define everything in a xml file and set the background of a view as the drawable xml. /drawable/myrotate.xml -> #drawable/myrotate
Edit:
This is an answer I found here.
Drawable Rotating around its center Android
Edit 2:
You are right the RotateDrawable seem broken. I don't know I tried it too. I haven't yet succeded in making it animate. But I did succed to rotate it. You have to use setLevel which will rotate it. Though it doesn't look really useful. I browsed the code and the RotateDrawable doesn't even inflate the animation duration and the current rotation seems strangely use the level as a measure for rotation. I believe you have to use it with a AnimationDrawable but here again. It just crashed for me. I haven't used that feature yet but planned to use it in the future. I browsed the web and the RotateDrawable seems to be very undocumented like almost every Drawable objects.
Here is one of possible ways (especially useful when you have a Drawable somewhere set and need to animate it). The idea is to wrap the drawable and decorate it with animation. In my case, I had to rotate it, so below you can find sample implementation:
public class RotatableDrawable extends DrawableWrapper {
private float rotation;
private Rect bounds;
private ObjectAnimator animator;
private long defaultAnimationDuration;
public RotatableDrawable(Resources resources, Drawable drawable) {
super(vectorToBitmapDrawableIfNeeded(resources, drawable));
bounds = new Rect();
defaultAnimationDuration = resources.getInteger(android.R.integer.config_mediumAnimTime);
}
#Override
public void draw(Canvas canvas) {
copyBounds(bounds);
canvas.save();
canvas.rotate(rotation, bounds.centerX(), bounds.centerY());
super.draw(canvas);
canvas.restore();
}
public void rotate(float degrees) {
rotate(degrees, defaultAnimationDuration);
}
public void rotate(float degrees, long millis) {
if (null != animator && animator.isStarted()) {
animator.end();
} else if (null == animator) {
animator = ObjectAnimator.ofFloat(this, "rotation", 0, 0);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
}
animator.setFloatValues(rotation, degrees);
animator.setDuration(millis).start();
}
#AnimatorSetter
public void setRotation(float degrees) {
this.rotation = degrees % 360;
invalidateSelf();
}
/**
* Workaround for issues related to vector drawables rotation and scaling:
* https://code.google.com/p/android/issues/detail?id=192413
* https://code.google.com/p/android/issues/detail?id=208453
*/
private static Drawable vectorToBitmapDrawableIfNeeded(Resources resources, Drawable drawable) {
if (drawable instanceof VectorDrawable) {
Bitmap b = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
drawable.setBounds(0, 0, c.getWidth(), c.getHeight());
drawable.draw(c);
drawable = new BitmapDrawable(resources, b);
}
return drawable;
}
}
and you can use it like this (rotating toolbar navigation icon 360 degrees):
backIcon = new RotatableDrawable(getResources(), toolbar.getNavigationIcon().mutate());
toolbar.setNavigationIcon(backIcon);
backIcon.rotate(360);
It shouldn't be hard to add a method that will rotate it indefinite (setRepeatMode INFINITE for animator)
You can start from studying the ProgressBar2 from API Demos project (it is available as a part of the SDK). Specifically pay attention to R.layout.progressbar_2.
I take back to life this post just to post my solution with vector drawable:
So you need 1 vector drawable in drawable resource (#drawable/ic_brush_24dp):
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<group
android:name="rotation" <---- target name of animated-vector
android:pivotX="12.0"
android:pivotY="12.0"
android:rotation="0.0"> <--- propertyName of animator
<path
android:fillColor="#FF000000"
android:pathData="M7,14c-1.66,0 -3,1.34 -3,3 0,1.31 -1.16,2 -2,2 0.92,1.22 2.49,2 4,2 2.21,0 4,-1.79 4,-4 0,-1.66 -1.34,-3 -3,-3zM20.71,4.63l-1.34,-1.34c-0.39,-0.39 -1.02,-0.39 -1.41,0L9,12.25 11.75,15l8.96,-8.96c0.39,-0.39 0.39,-1.02 0,-1.41z" />
</group>
</vector>
Then you need your animator in animator resource folder (#animator/pendulum)
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:duration="350"
android:repeatCount="infinite"
android:repeatMode="reverse"
android:propertyName="rotation"
android:valueFrom="0.0"
android:valueTo="-90.0" />
</set>
Then you need your animated-vector in drawable resource (#drawable/test_anim_brush2):
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="#drawable/ic_brush_24dp">
<target
android:name="rotation"
android:animation="#animator/pendulum" />
</animated-vector>
Then you need to extend ImageView (because that's the only way i found to start the animation)
public class ImageView extends AppCompatImageView{
public ImageView(Context context){
super(context);
init(context, null, 0);
}
public ImageView(Context context, AttributeSet attrs){
super(context, attrs);
init(context, attrs, 0);
}
public ImageView(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
init(context, attrs, 0);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr){
}
#Override
protected void onAttachedToWindow(){
super.onAttachedToWindow();
Drawable d = getBackground();
if(d instanceof AnimatedVectorDrawable){
AnimatedVectorDrawable anim = (AnimatedVectorDrawable)d;
anim.start();
}
}
#Override
protected void onDetachedFromWindow(){
super.onDetachedFromWindow();
Drawable d = getBackground();
if(d instanceof AnimatedVectorDrawable){
AnimatedVectorDrawable anim = (AnimatedVectorDrawable)d;
anim.stop();
}
}
}
And then add your imageView to your layout :
<ui.component.ImageView
android:id="#+id/test_anim_brush"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_horizontal"
android:background="#drawable/test_anim_brush2"/>
Pro :
You can fully define your animation in xml with object animator (duration, interpolator, etc...)
Works with everything which accept drawable (as long as you add at good place the start animator)
Con:
Works only with vector
Still need to add your custom start by extending class or whenever you think it is smart...
private ValueAnimator rotateDrawable(RotateDrawable drawable, int fromDegree, int toDegree) {
drawable.setFromDegrees(fromDegree);
drawable.setToDegrees(toDegree);
return ObjectAnimator.ofInt(drawable, "level", 0, 10000);
}
level is the interpulting value from 0 to 100000. the actual animation values are set by the setter methods
you can use stateListAnimator
<ImageView
android:id="#+id/your_id"
android:layout_width="200dp"
android:layout_height="200dp"
android:src="#drawable/your_drawable"
android:stateListAnimator="#anim/bounce" />
and bounce
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="#android:anim/bounce">
<translate
android:duration="900"
android:fromXDelta="100%p"
android:toXDelta="0%p" />
</set>