I've come from an iOS background where there was a property called transform which was essentially just a 4x4 matrix for a 3D transformation that is applied to a CALayer (Also UIView).
I have come across the Transformation class, which seems to be a bit different, but I'm hoping that I can use it for some transformations on a View subclass.
I have seen the constructor new Transformation() and the different set methods, but I can't see anything that will allow me to actually set the 3x3 matrix for the Transformation or how it is used on a View.
I would guess that this isn't a very important class, either, as there seem to be errors in the document itself.
I was hoping that someone on SO might be able to help me figure out how to utilize this class to get transformations working on my View subclasses (How to construct and apply a Transformation) or point me towards a better tool/class for my needs.
You'd typically apply a transformation within an Animation object in Android. i.e. Create a new Animation class that sets a transform for your view:
public class myAnimation extends Animation {
public myAnimation() { }
#Override
boolean willChangeTransformationMatrix() { return true; }
#Override
void applyTransformation(float interpolatedTime, Transformation t) {
Matrix m = t.getMatrix();
// do whatever you want to the matrix here, using the interpolatedTime
}
}
Then set the animation on your view:
Animation anim = new myAnimation();
anim.setDuration(100);
view.startAnimation(anim);
On post Honeycomb Android, you should be able to do similar things just by manipulating the matrix on a view though:
Matrix m = view.getMatrix();
// do whatever you want here
Related
I want to create skew animation for a View in android.
So, I have rectangular view (this is normal/start state), at the end it should looks like this:
I can do such a thing with android.view.animation package using Transformation of TYPE_MATRIX. So I can create Matrix and use preSkew method, apply this Matrix to my custom animation and then start this animation on a view. So this is OK.
BUT
The problem is I want to do this with ObjectAnimator.
Because using android.view.animation package is not recommended to use.
But I can't get the solution for this problem with ObjectAnimator class.
If I have ImageView, I can use method setImageMatrix for ObjectAnimator (and also create custom MatrixEvaluator to evaluate corresponding matrices for each factor),
but I want to get solution for this problem with a View class, not for some subclass of it.
And the main reason is, that TextView, for example, doesn't have public method like setMatrix, and I want to get the solution for TextView too. So get the solution for base View class is the main desire.
There is another solution: create custom, for example, SkewTextView, in which onDraw method we use canvas.skew method.
But this is worse solution even than using android.view.animation package, because you need to create wrappers for any View subclass you want to skew.
So, I don't know, how I can skew View with a ObjectAnimator (as View class doesn't have public methods like setSkew or setMatrix).
How would you solve this problem?
Have you any idea of how to solve this problem?
Is there a solution for this problem with ObjectAnimator?
Any help (or even thoughts) is greatly appreciated
Thanks
Well without subclassing this might be a bit tricky. My main idea is to use TimeAnimator, which will provide you time callbacks in conjunction with Camera (which can use Matrix transformations). Or you can use TimeAnimator and make a Bitmap from the View you want to skew, make the original View invisible, and draw the Bitmap on each TimeAnimator step applying a Matrix to it. But I'm not sure if this is the optimal way to do that, since you will have to create a new Bitmap on each frame
EDIT: No, Camera approach is not the way to do that as it is stated in comments below
I created a skew animation using Android Property Animation APIs in my project (GitHub Link).
What I did:
Created a custom view (SkewView) and added a SKEW_X property to it.
public class SkewView extends ImageView {
private static final String TAG = "SkewView";
public static final Property SKEW_X
= new FloatProperty("skewX") {
#Override
public void setValue(SkewView object, float value) {
object.setSkewX(value);
}
#Override
public Float get(SkewView object) {
return object.getSkewX();
}
};
private float mSkewX;
// ...
public float getSkewX() {
return mSkewX;
}
public void setSkewX(float skewX) {
mSkewX = skewX;
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
if (mSkewX != 0) {
canvas.skew((float) (mSkewX * Math.PI / 180.0f), 0);
}
super.onDraw(canvas);
}
}
Used ObjectAnimator to animate this property.
ObjectAnimator skewAnimator = ObjectAnimator.ofFloat(mTarget, SkewView.SKEW_X, 30);
I have a custom View where I implement onDraw and it successfully draws the background as well as a small overlay image at the top right. I animate that overlay image (AnimationSet with a ScaleAnimation and Rotate Animation) manually by doing:
final AnimationSet animationSet = new AnimationSet(false);
animationSet.addAnimation(scaleAnimation);
animationSet.addAnimation(rotationAnimation);
animationSet.setFillAfter(true);
post(new Runnable() {
#Override
public void run () {
animationSet.start();
new Handler().post(getAnimationRunnable(animationSet));
}
private Runnable getAnimationRunnable (final AnimationSet animationSet) {
return new Runnable() {
#Override
public void run () {
final Transformation outTransformation = new Transformation();
if (animationSet.getTransformation(System.currentTimeMillis(), outTransformation)) {
// store matrix for applying in onDraw
mMatrix = outTransformation.getMatrix();
// THIS INVALIDATE DOESN'T WORK
parent.invalidate(new Rect(getParentInvalidateRect()));
post(getAnimationRunnable(animationSet));
}
}
};
}
});
If I invalidate the custom View like this ...
// THIS INVALIDATE DOES WORK
invalidate(getOverlayClientRect());
then the animation happens and all is great, except that the animated image now gets clipped - because it's being scaled up, beyond the top and right bounds.
But when I pass a Rect to parent.invalidate() that intersects the custom View's top right corner area, it does not trigger an onDraw for the custom View.
So ...
will this approach work?
if so, is there any other factor than the Rect passed to parent.invalidate() that determines whether it will call the custom Views onDraw method?
After a lot of searching, the answer was obvious. I just had to set the clipChildren property of the parent to false. Didn't need to invalidate the parent, just the custom View.
That worked fine on a 4.0.3 tablet. However, on a Galaxy Nexus (4.2.2), not only did it render outside of the bounds during the animation, it actually only cleared the background of the Rect passed to invalidate. So, when the animation was done some remnants of the image were still visible outside the Rect. The solution there was to pass a Rect large enough to cover the largest of the drawn bitmaps during animation.
I've been working on an app with complex layouts. I recently realized I need to make parts or all of my layouts zoomable.
One of my main xml files has a linear layout with multiple layouts nested within it to positions views properly. Is there an easy way to make this linear layout and everything within in zoomable? Or would it be easier to make the entire layout file zoomable? What are my options?
First of all extend that class with that specific view
public class MyImageView extends ImageView{
Override following method .
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.scale(mScaleFactor, mScaleFactor, midPoint.x, midPoint.y);
if(appInitialized) {
hsSide.draw(canvas);
scaleA.draw(canvas);
scaleB.draw(canvas);
}
canvas.restore();
}
Create a Gesture Detector that will detect size of zoomed object and you can limit to avoid overlaps.
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
mScaleFactor *= detector.getScaleFactor();
pivotX = detector.getFocusX();
pivotY = detector.getFocusY();
// Don't let the object get too small or too large.
mScaleFactor = Math.max(0.8f, Math.min(mScaleFactor, 2.0f));
invalidate();
return true;
}
}
At the end initialize the object
ScaleGestureDetector mScaleDetector;
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
You might have a look at static transformations. Any ViewGroup or subclass can be customized to apply a transformation to its child views. You enable this by calling setStaticTransformationsEnabled(true), and then overriding the getChildStaticTransformation() callback (docs link) in your custom ViewGroup. You can apply any transformation you like, including a scale to create your zoom effect. This callback will be called any time the view needs to be redrawn or is invalidated.
Also, beware when using this alongside hardware acceleration. Depending on the frequency with which you need to update the transformations you may find hardware doesn't quite work to redraw as you expect. If so, you will need to enable software layers for this view hierarchy.
I know multiple ways to get location values of a View.
getLocationOnScreen()
getLocationInWindow()
getLeft()
However, none of them actually returns the current location of the View I moved by startAnimation() method, but only the original location.
So, now let's make a View that moves to the right by 10 pixels on each Click (I'm omitting the layout, since you can just place whatever view in your main XML and give it onClickListener).
public class AndroidTestActivity extends Activity implements OnClickListener {
LinearLayout testView;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
testView = (LinearLayout) this.findViewById(R.id.test);
testView.setOnClickListener(this);
}
public void onClick(View v) {
int[] pLoS = new int[2];
testView.getLocationOnScreen(pLoS);
TranslateAnimation move = new TranslateAnimation(pLoS[0], pLoS[0] + 10, 0f, 0f);
move.setFillAfter(true);
move.setFillEnabled(true);
testView.startAnimation(move);
}
}
As you see, this doesn't work as I intended, since getLocationOnScreen() always returns the same value (in my case, 0), and doen't reflect the value I used in TranslateAnimation...
Any idea?
Assuming you're using Android < 3.0 then your question may be in a similar vein to mine I asked here. Basically Animations are separate from the View itself i.e. Android animates a copy of your View. That is why getLocationOnScreen() always returns 0. It's not the view that has moved (animated) it was the copy that moved (animated). If you see the answers to my question this issue has been addressed in later versions of Android.
Well, if you are trying to see how many pixels the view has shifted during/after its animation, then you can open up the Transformation object on the animation.
Here's an example using the AnimationListener:
animation.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
Transformation trans = new Transformation();
// startTime + duration = end of animation
int endTime = animation.getStartTime()+animation.getDuration();
animation.getTransformation(endTime, trans);
Matrix transformationMatrix = trans.getMatrix();
float[] matrixVals = new float[9];
transformationMatrix.getValues(matrixVals);
float xTraveled = matrixVals[2];
float yTraveled = matrixVals[5];
// do something with them here...
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
I may not be exactly correct in the array indices for these, definitely use Matrix.toString() and look at the values yourself. xTraveled and yTraveled will give you the amount of distance traveled by the TranslateAnimation at the indicated time (in this case, at the end of the animation).
animations on android gingerbread and below do not really change the view in any way , only change the way it is shown.
the only way to get the new position is by calculating it.
For my activity i use 3 custom views stacked.
the lower one is a SurfaceView fullscreen, the middle one is derived from GridView and overrides onDraw to add custom graphic.
The top one is derived directly from View, sits in a corner of the screen and act as a knob to control the others two views (this is the problematic view).
to add a custom animation to this view, i used this pattern:
public class StubKnobView extends View{
private Drawable knob;
private Point knobPos;
private float rotation;
private boolean needsToMove;
public void moveKnob(){
/* ...various calculations... */
this.needsToMove=true;
invalidate(); //to notify the intention to start animation
}
private void updateAnimation(){
/* ........... */
this.needsToMove= !animationComplete;
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
int saveCount=canvas.getSaveCount();
canvas.save();
canvas.rotate(this.rotation, this.knobPos.x, this.knobPos.y );
this.knob.draw(canvas);
canvas.restoreToCount(saveCount);
if(this.needsToMove){
updateAnimation();
}
}
}
ideally, if there is an animation pending, after each drawing cycle the view should auto invalidate.
right now this doesn't work, to force the animation i have to touch the screen to cause a onDraw cycle.
Using "show screen updates" of Dev tools I see that no screen invalidate/update cycle happen , apart when i click the screen.
specifying the dirty rect also ha no effect.
So, any idea where to look to know why this invalidate/draw cycle does not work the way is intended?
I encontered this situation, and also doubted that invalidate() doesn't make onDraw() called again.
After simplified the business codes, it's turn out that there is a dead loop - exactly too much iterations, which blocks the UI thread.
So, my advice is that make sure the UI thread going smoothly first.
Try something like this with a private class in your View. The idea is not to call invalidate() from within onDraw(). Instead, post it on the running queue. Instantiate the private class in your View constructor and then just call animateKnob.start() when you want the animation. The onDraw() can then just focus on drawing.
public void moveKnob(){
// update the state to draw... KnobPos, rotation
invalidate()
}
private class AnimateKnob{
public void start(){
// Do some calculations if necessary
// Start the animation
MyView.this.post( new Runnable() {
stepAnimation()
});
}
public void stepAnimation(){
// update the animation, maybe use an interpolator.
// Here you could also determine if the animation is complete or not
MyView.this.moveKnob();
if( !animationComplete ){
// Call it again
MyView.this.post( new Runnable() {
stepAnimation()
});
}
}
}