I have an infinite translate animation applied to an ImageView:
Animation animation = new TranslateAnimation(0, 0, -500, 500);
animation.setDuration(4000);
animation.setFillAfter(false);
myimage.startAnimation(animation);
animation.setRepeatCount(Animation.INFINITE);
What I have noticed is that the translation process is slower when the image is near the starting and ending point compared to when its near half the distance (middle point).
I guess the velocity for translate animations on android is not uniform.
How do I make the velocity uniform throughout the process ?
I did some source-diving to investigate this. First, note that if a linear interpolator is used to provide interpolatedTime values to the applyTransformation method of TranslateAnimation, the resulting translation will have constant velocity (because the offsets dx and dy are linear functions of interpolatedTime (lines 149-160)):
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float dx = mFromXDelta;
float dy = mFromYDelta;
if (mFromXDelta != mToXDelta) {
dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
}
if (mFromYDelta != mToYDelta) {
dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
}
t.getMatrix().setTranslate(dx, dy);
}
applyTransformation is called by the getTransformation method of the base Animation class (lines 869-870):
...
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
applyTransformation(interpolatedTime, outTransformation);
...
According to the documentation for the setInterpolator method (lines 382-392), mInterpolator should default to a linear interpolator:
/**
* Sets the acceleration curve for this animation. Defaults to a linear
* interpolation.
*
* #param i The interpolator which defines the acceleration curve
* #attr ref android.R.styleable#Animation_interpolator
*/
public void setInterpolator(Interpolator i) {
mInterpolator = i;
}
However, this seems to be false: both constructors in the Animation class call the ensureInterpolator method (lines 803-811):
/**
* Gurantees that this animation has an interpolator. Will use
* a AccelerateDecelerateInterpolator is nothing else was specified.
*/
protected void ensureInterpolator() {
if (mInterpolator == null) {
mInterpolator = new AccelerateDecelerateInterpolator();
}
}
which suggests that the default interpolator is an AccelerateDecelerateInterpolator. This explains the behavior you describe in your question.
To actually answer your question, then, it would appear that you should amend your code as follows:
Animation animation = new TranslateAnimation(0, 0, -500, 500);
animation.setInterpolator(new LinearInterpolator());
animation.setDuration(4000);
animation.setFillAfter(false);
myimage.startAnimation(animation);
animation.setRepeatCount(Animation.INFINITE);
Related
I am looking for a way to connect pan gesture with percentage of animation completion. Let me show you what I mean.
This image represents an animation that I want to execute, namely a moving Image actor or a sprite. The animation gets executed by pan gesture. Animation is 100% complete and at stage 6 when user slides for a 200px. If user slided only 100px, it would be 50% complete and at stage 3. If the user didn't execute pan gesture the animation stays at 0% and at stage 1. I am looking for tips on how to start building such a model. I believe it is called interactive. Do you have any suggestions?
You can use a GestureDetector to handle the panning input. The gestureListener.pan method can update an animation position parameter.
private int screenPixelsToAnimationPositionRatio = 0.01f; //will need to tweak this and probably adjust it at runtime based on screen dimensions and resolution
private float animationPosition = 0; //from 0 to 1, fraction of animation complete
public void create () {
//...
GestureAdapter gestureAdapter = new GestureAdapter {
#Override
public boolean pan (float x, float y, float deltaX, float deltaY) {
animationPosition += deltaX * screenPixelsToAnimationPositionRatio;
animationPosition = MathUtils.clamp(animationPosition, 0, 1);
return true;
}
};
GestureDetector gestureDetector = new GestureDetector(gestureAdapter);
Gdx.input.setInputProcessor(gestureDetector); //or put the detector in an InputMultiplexer with your other input listeners.
}
Then you would create a method that can update your object's position and rotation based on the current value of animationPosition. You would need to figure out the equations that determine the movement you want. For example, something that looks sort of like what you illustrated above:
private void updateAnimation (){
x = animationPosition * 30;
float y = 0, rotation = 0;
if (animationPosition >= 0.25f) {
float jumpPosition = Math.min(1, (animationPosition - 0.25f) / 0.5f);
y = 30 * (1 - Interpolation.pow2In.apply(Math.abs(2 * jumpPosition - 1)));
rotation = 180 * jumpPosition;
}
mySprite.setPosition(x, y);
mySprite.setRotation(rotation);
}
Then call this update method somewhere in render.
I want to apply a translate animation on an Android view (button) using a custom interpolator where the easing function is:
public static float easeOut(float t,float b , float c, float d) {
if ((t/=d) < (1/2.75f)) {
return c*(7.5625f*t*t) + b;
} else if (t < (2/2.75f)) {
return c*(7.5625f*(t-=(1.5f/2.75f))*t + .75f) + b;
} else if (t < (2.5/2.75)) {
return c*(7.5625f*(t-=(2.25f/2.75f))*t + .9375f) + b;
} else {
return c*(7.5625f*(t-=(2.625f/2.75f))*t + .984375f) + b;
}
}
I have an example that uses the custom interpolator like this:
The interpolator is:
public class HesitateInterpolator implements Interpolator {
public HesitateInterpolator() {
}
public float getInterpolation(float t) {
float x = 2.0f * t - 1.0f;
return 0.5f * (x * x * x + 1.0f);
}
}
and is used like this:
ScaleAnimation anim = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f);
anim.setInterpolator(new HesitateInterpolator());
My question is:
What are these values b, c, d for?
FYI: for people who just want an ease interpolator you can just use myAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
I made a library which can solve this problem. AndroidEasingFunctions
1,2,3 go
Create a custom cubic bezier curve using this awesome site. And get the control points for the curve. Between 0,0 and 1,1
Interpolator customInterpolator = PathInterpolatorCompat.create(cpX1,cpX2,cpY1,cpY2)
Add this customInterpolator to any of your animation.
Added a gist.
Some more here.
According to Robert Penner's Easing Functions, as stated here:
t: current time, b: begInnIng value, c: change In value, d: duration
If you want to implement your custom Interpolator, you have to make something like this:
(this would be the implementation for the easeInOutQuint)
public class MVAccelerateDecelerateInterpolator implements Interpolator {
// easeInOutQuint
public float getInterpolation(float t) {
float x;
if (t<0.5f)
{
x = t*2.0f;
return 0.5f*x*x*x*x*x;
}
x = (t-0.5f)*2-1;
return 0.5f*x*x*x*x*x+1;
}
}
Edit:
to implement the easing function you need some math knowledge, considering that the getInterpolation method gets only the t parameter, from 0.0 to 1.0.
So basically you need to develop a y(t) function, with t from 0 to 1, and with y values from 0 to 1, as shown below:
What you change is the curve to get from 0 to 1 (in the image the green line is the linear one, for example). You need to 'normalize' the easing functions to remain in the (0, 1) x (0, 1) square, as you can see in my easeInOutQuint implementation.
Here is another solution, made possible by a recent addition to android's support library:
https://gist.github.com/ebabel/8ff41cad01e9ce1dd9ce
and an example in use:
public static void expand(final View v) {
v.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int targetHeight = v.getMeasuredHeight();
if ( v.getHeight() != targetHeight ) {
// Older versions of android (pre API 21) cancel animations for views with a height of 0 so use 1 instead.
v.getLayoutParams().height = 1;
v.setVisibility(View.VISIBLE);
Animation a = new Animation() {
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
v.getLayoutParams().height = interpolatedTime == 1
? ViewGroup.LayoutParams.WRAP_CONTENT
: (int) (targetHeight * interpolatedTime);
v.requestLayout();
}
#Override
public boolean willChangeBounds() {
return true;
}
};
a.setInterpolator(EasingsConstants.easeInOutQuart);
a.setDuration(computeDurationFromHeight(v));
v.startAnimation(a);
} else {
Log.d("AnimationUtil", "expand Already expanded ");
}
}
/**
* 1dp/ms * multiplier
*/
private static int computeDurationFromHeight(View v) {
return (int) (v.getMeasuredHeight() / v.getContext().getResources().getDisplayMetrics().density) * DURATION_MULTIPLIER;
}
I'm implementing a kind of a "compass arrow" that follows destination depending on physical orientation of the device using magnetic field sensor. Suddenly I faced with a little problem.
Obtaining bearing and azimuth is OK, but performing a realistic animation turned into a really hard task. I tried to use different interpolators to make animation more "physical" (i. e. as in real compass, which arrow oscillate after hairpin rotation, accelerate and decelerate during movement etc).
Now I'm using interpolator.accelerate_decelerate and everything is quite good until updates start arriving quickly. That makes animations overlap each other and the arrow becomes twitchy and nervous. I want to avoid this. I tried to implement a queue to make every next animation wait until previous ends, or drop updates that come very quickly. That made animation look smooth, but arrow's behavior turned into absolutely illogical.
So I have 2 questions:
1) is there some way to make animated transitions more smooth in the cases when animations overlap each other?
2) is there a way to stop animation that is currently processing and get intermediate position of an object?
My code is below. An UpdateRotation() method handles orientation and bearing updates and executes animation of external viewArrow view.
public class DirectionArrow {
// View that represents the arrow
final View viewArrow;
// speed of rotation of the arrow, degrees/sec
final double rotationSpeed;
// current values of bearing and azimuth
float bearingCurrent = 0;
float azimuthCurrent = 0;
/*******************************************************************************/
/**
* Basic constructor
*
* #param view View representing an arrow that should be rotated
* #param rotationSpeed Speed of rotation in deg/sec. Recommended from 50 (slow) to 500 (fast)
*/
public DirectionArrow(View view, double rotationSpeed) {
this.viewArrow = view;
this.rotationSpeed = rotationSpeed;
}
/**
* Extended constructor
*
* #param viewArrow View representing an arrow that should be rotated
* #param rotationSpeed Speed of rotation in deg/sec. Recommended from 50 (slow) to 500 (fast)
* #param bearing Initial bearing
* #param azimuth Initial azimuth
*/
public DirectionArrow(View viewArrow, double rotationSpeed, float bearing, float azimuth){
this.viewArrow = viewArrow;
this.rotationSpeed = rotationSpeed;
UpdateRotation(bearing, azimuth);
}
/**
* Invoke this to update orientation and animate the arrow
*
* #param bearingNew New bearing value, set >180 or <-180 if you don't need to update it
* #param azimuthNew New azimuth value, set >360 or <0 if you don't need to update it
*/
public void UpdateRotation(float bearingNew, float azimuthNew){
// look if any parameter shouldn't be updated
if (bearingNew < -180 || bearingNew > 180){
bearingNew = bearingCurrent;
}
if (azimuthNew < 0 || azimuthNew > 360){
azimuthNew = azimuthCurrent;
}
// log
Log.println(Log.DEBUG, "compass", "Setting rotation: B=" + bearingNew + " A=" + azimuthNew);
// calculate rotation value
float rotationFrom = bearingCurrent - azimuthCurrent;
float rotationTo = bearingNew - azimuthNew;
// correct rotation angles
if (rotationFrom < -180) {
rotationFrom += 360;
}
while (rotationTo - rotationFrom < -180) {
rotationTo += 360;
}
while (rotationTo - rotationFrom > 180) {
rotationTo -= 360;
}
// log again
Log.println(Log.DEBUG, "compass", "Start Rotation to " + rotationTo);
// create an animation object
RotateAnimation rotateAnimation = new RotateAnimation(rotationFrom, rotationTo,
Animation.RELATIVE_TO_SELF, (float) 0.5, Animation.RELATIVE_TO_SELF, (float) 0.5);
// set up an interpolator
rotateAnimation.setInterpolator(viewArrow.getContext(), interpolator.accelerate_decelerate);
// force view to remember its position after animation
rotateAnimation.setFillAfter(true);
// set duration depending on speed
rotateAnimation.setDuration((long) (Math.abs(rotationFrom - rotationTo) / rotationSpeed * 1000));
// start animation
viewArrow.startAnimation(rotateAnimation);
// update cureent rotation
bearingCurrent = bearingNew;
azimuthCurrent = azimuthNew;
}
}
Here is my custom ImageDraw class where I implemted physical behavior of the pointing arrow based on equation of circular motion of dipole in magnetic field.
It don't uses any animators nor interpolators--on every iteration angular position is recalculated based on physical parameters. These parameters can be widely adjusted via setPhysical method. For example, to make rotations more smooth and slow, increase alpha (damping coefficient), to make arrow more responsitive, increase mB (coefficient of magnetic field), to make arrow oscillate on rotations, increase inertiaMoment.
Animation and redraw is performed implicitly by invoke of invalidate() on every iteration. There is no need to handle it explicitly.
To update the angle at which the arrow should rotate, just call rotationUpdate (by user's choice or using device orientation sensor callback).
/**
* Class CompassView extends Android ImageView to perform cool, real-life animation of objects
* such compass needle in magnetic field. Rotation is performed relative to the center of image.
*
* It uses angular motion equation of magnetic dipole in magnetic field to implement such animation.
* To vary behaviour (damping, oscillation, responsiveness and so on) set various physical properties.
*
* Use `setPhysical()` to vary physical properties.
* Use `rotationUpdate()` to change angle of "magnetic field" at which image should rotate.
*
*/
public class CompassView extends ImageView {
static final public float TIME_DELTA_THRESHOLD = 0.25f; // maximum time difference between iterations, s
static final public float ANGLE_DELTA_THRESHOLD = 0.1f; // minimum rotation change to be redrawn, deg
static final public float INERTIA_MOMENT_DEFAULT = 0.1f; // default physical properties
static final public float ALPHA_DEFAULT = 10;
static final public float MB_DEFAULT = 1000;
long time1, time2; // timestamps of previous iterations--used in numerical integration
float angle1, angle2, angle0; // angles of previous iterations
float angleLastDrawn; // last drawn anglular position
boolean animationOn = false; // if animation should be performed
float inertiaMoment = INERTIA_MOMENT_DEFAULT; // moment of inertia
float alpha = ALPHA_DEFAULT; // damping coefficient
float mB = MB_DEFAULT; // magnetic field coefficient
/**
* Constructor inherited from ImageView
*
* #param context
*/
public CompassView(Context context) {
super(context);
}
/**
* Constructor inherited from ImageView
*
* #param context
* #param attrs
*/
public CompassView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Constructor inherited from ImageView
*
* #param context
* #param attrs
* #param defStyle
*/
public CompassView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* onDraw override.
* If animation is "on", view is invalidated after each redraw,
* to perform recalculation on every loop of UI redraw
*/
#Override
public void onDraw(Canvas canvas){
if (animationOn){
if (angleRecalculate(new Date().getTime())){
this.setRotation(angle1);
}
} else {
this.setRotation(angle1);
}
super.onDraw(canvas);
if (animationOn){
this.invalidate();
}
}
/**
* Use this to set physical properties.
* Negative values will be replaced by default values
*
* #param inertiaMoment Moment of inertia (default 0.1)
* #param alpha Damping coefficient (default 10)
* #param mB Magnetic field coefficient (default 1000)
*/
public void setPhysical(float inertiaMoment, float alpha, float mB){
this.inertiaMoment = inertiaMoment >= 0 ? inertiaMoment : this.INERTIA_MOMENT_DEFAULT;
this.alpha = alpha >= 0 ? alpha : ALPHA_DEFAULT;
this.mB = mB >= 0 ? mB : MB_DEFAULT;
}
/**
* Use this to set new "magnetic field" angle at which image should rotate
*
* #param angleNew new magnetic field angle, deg., relative to vertical axis.
* #param animate true, if image shoud rotate using animation, false to set new rotation instantly
*/
public void rotationUpdate(final float angleNew, final boolean animate){
if (animate){
if (Math.abs(angle0 - angleNew) > ANGLE_DELTA_THRESHOLD){
angle0 = angleNew;
this.invalidate();
}
animationOn = true;
} else {
angle1 = angleNew;
angle2 = angleNew;
angle0 = angleNew;
angleLastDrawn = angleNew;
this.invalidate();
animationOn = false;
}
}
/**
* Recalculate angles using equation of dipole circular motion
*
* #param timeNew timestamp of method invoke
* #return if there is a need to redraw rotation
*/
protected boolean angleRecalculate(final long timeNew){
// recalculate angle using simple numerical integration of motion equation
float deltaT1 = (timeNew - time1)/1000f;
if (deltaT1 > TIME_DELTA_THRESHOLD){
deltaT1 = TIME_DELTA_THRESHOLD;
time1 = timeNew + Math.round(TIME_DELTA_THRESHOLD * 1000);
}
float deltaT2 = (time1 - time2)/1000f;
if (deltaT2 > TIME_DELTA_THRESHOLD){
deltaT2 = TIME_DELTA_THRESHOLD;
}
// circular acceleration coefficient
float koefI = inertiaMoment / deltaT1 / deltaT2;
// circular velocity coefficient
float koefAlpha = alpha / deltaT1;
// angular momentum coefficient
float koefk = mB * (float)(Math.sin(Math.toRadians(angle0))*Math.cos(Math.toRadians(angle1)) -
(Math.sin(Math.toRadians(angle1))*Math.cos(Math.toRadians(angle0))));
float angleNew = ( koefI*(angle1 * 2f - angle2) + koefAlpha*angle1 + koefk) / (koefI + koefAlpha);
// reassign previous iteration variables
angle2 = angle1;
angle1 = angleNew;
time2 = time1;
time1 = timeNew;
// if angles changed less then threshold, return false - no need to redraw the view
if (Math.abs(angleLastDrawn - angle1) < ANGLE_DELTA_THRESHOLD){
return false;
} else {
angleLastDrawn = angle1;
return true;
}
}
Are you filtering your sensor data? The Magnetometer is a pain low pass filtering isn't really enough. You could use weighted-smoothing or maybe rounding data would be helpful:
Math.round( xyz * 10) / 10; ?
You could also reduce the frequency at which you get sensor updates. That might help.
mSensorManager.registerListener(this, mMagnetometer, 10000);
Espessially for gilonm, nice implementation of fixed size queue and getting its mean value:
float queue[ARRAY_LENGTH] = {0};
int queueFront = queue.length - 1 // position of front element
float meanValue = 0; // calculated mean value
float pushNewAndGetMean(float newValue){
// recalculate mean value
meanValue = meanValue + (newValue - queue[queueFront]) / queue.length;
// overwrite value in front pointer position
queue[queueFront] = newValue;
// shift front pointer 1 step right or to '0' if end of array reached
queueStart = (queueFront + 1) % array.length;
return meanValue
};
Here, not dependent on array length, you make just 2 reassignments of variables (instead of N) and use only 3 elements in mean calculation (instead of N). This makes algorithm O(1) complexity instead of O(N).
What you could do is where you get your data from the sensors - you can just use and array to do an average of say last 5 readings - that should smooth things down.
something like this:
Declare an array private float azimArray[] = {0,0,0,0,0};
Now where you get sensor data, use:
azimArray[0] = azimArray[1];
azimArray[1] = azimArray[2];
azimArray[2] = azimArray[3];
azimArray[3] = azimArray[4];
azimArray[4] = event.values[0]; //get actual sensor data into last array cell
currentAzimuth = Math.round(azimArray[0]+azimArray[1]+azimArray[2]+azimArray[3]+azimArray[4]/5);
Now currentAzimuth holds the rounded average of last 5 readings, which should smooth things down for you.
Hope this helped!
I am using this Rotate3dAnimation class to create a flipping coin animation which is also moving and scaling. But i can use it only with one image view. By just using startAnimation() method on that image view.
But What i want to do is, to use two sides of a coin so it will look like a real coin with two different sides is flipping. Can someone help me about how to do that?
Thanks
package com.example.movingcoin;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.graphics.Camera;
import android.graphics.Matrix;
/**
* An animation that rotates the view on the Y axis between two specified angles.
* This animation also adds a translation on the Z axis (depth) to improve the effect.
*/
public class Rotate3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;
/**
* Creates a new 3D rotation on the Y axis. The rotation is defined by its
* start angle and its end angle. Both angles are in degrees. The rotation
* is performed around a center point on the 2D space, definied by a pair
* of X and Y coordinates, called centerX and centerY. When the animation
* starts, a translation on the Z axis (depth) is performed. The length
* of the translation can be specified, as well as whether the translation
* should be reversed in time.
*
* #param fromDegrees the start angle of the 3D rotation
* #param toDegrees the end angle of the 3D rotation
* #param centerX the X center of the 3D rotation
* #param centerY the Y center of the 3D rotation
* #param reverse true if the translation should be reversed, false otherwise
*/
public Rotate3dAnimation(float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ, boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
}
#Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
camera.save();
if (mReverse) {
camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
} else {
camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
}
// camera.rotateY(degrees);
camera.rotateX(degrees);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
faced the same problem few days ago, found the solution in class FlipAnimator that you can find here: FlipAnimatorClass
it is pretty easy, actually: you just have to pass to FlipAnimator the two sides of the coin. The class is pretty easy to understand, I think, and it is practically doing what g00dy suggested in his comment above.
the trick is to rotate your view twice !
once from normal position to middle, change your view (eg. change the image of your coin) and then rotate it back from middle to normal.
you should do all the changes in view and starting rotation from middle to normal in the onAnimationEnd in AnimationListener of the first animation !
like this:
firstAnimation.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
findViewById(R.id.conceptsLay).setVisibility(View.GONE);
findViewById(R.id.factBaseLay).setVisibility(View.VISIBLE);
secondAnimation.startAnimation();
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
in the code above, I first rotate the conceptsLay to middle, where its basically invisible, then make it GONE and make the other examples view VISIBILE and start its animation from middle to normal!
so what the user sees is that the view is flipped !
needles to say that, first you will rotate it from 0 to 90, and then in secondAnimation, you will rotate it -90 to 0 !
also for making it smoother I've added some alpha animations as well!
hope it will help
I am trying to apply a ScaleAnimation to a View, but I have a somewhat nontraditional requirement: I would like to scale only a very specific region of the View.
Using the existing ScaleAnimation class I can easily uniformly scale a view. I can also set a pivot point about which to scale a view. Below is an example of that:
This is straightforward. But I wish to achieve the following, which results from scaling only a particular region of a view (or in this case a small horizontal rectangle in the middle of the smiley):
I dug around the source code for ScaleAnimation, and the following function seems to be responsible for scaling:
protected void applyTransformation(float interpolatedTime, Transformation t) {
float sx = 1.0f;
float sy = 1.0f;
if (mFromX != 1.0f || mToX != 1.0f) {
sx = mFromX + ((mToX - mFromX) * interpolatedTime);
}
if (mFromY != 1.0f || mToY != 1.0f) {
sy = mFromY + ((mToY - mFromY) * interpolatedTime);
}
if (mPivotX == 0 && mPivotY == 0) {
t.getMatrix().setScale(sx, sy);
} else {
t.getMatrix().setScale(sx, sy, mPivotX, mPivotY);
}
}
Since this function is simply applying a scale operation to a matrix, I was thinking there is some sort of matrix operation that I could use to write a custom scale animation, but my linear algebra know-how is lacking. The solution to this may be far simpler as it seems as though others would run into this issue, but I haven't been able to find any solutions. Thanks in advance.
A matrix transforms the entire bitmap. You can not scale regions with a single matrix.