I'm trying to create a custom surfaceview where every time the view is on the screen the view will start playing a video on its own. I was wondering what method within View is notified when a view is displayed on the UI and seen by the user. I'm using a viewpager so the SurfaceCreated doesn't work because views are created before they are displayed on the screen.
How to start a video automatically in a view pager when it comes on the screen
This was the underlying problem. The OP, wisely, wanted to try and isolate the point where it, in a sense "comes on to the screen". Problem is that this can mean many things:
When I first heard the question, I thought a good case would be onAttachedToWindow - see the docs. For people reading this question based on its original title, this is what you want.
The view is inflated and created in the Activity's onCreate in most cases (e.g. if you've used setContentView).
The OP had had no luck with surfaceCreated callbacks either. So we considered in the comments above whether the OP would be interested in the three draw stages layout, measure and draw. There are two stages to actually "putting a view on the screen" in android - the measuring, and the layout pass- see here.
Problem would be that it turned out that the OP was animating his view onto the screen, so the question became how do you tell when a view "arrives" on the screen after animation.
The important point is: you actually wanted to detect a stage much much later in the drawing process, which is understandable! Animation works by many calls to invalidate which in turn require many draws for that view's Canvas - so the stage at which you want to play the video is by no means when the view is first displayed in the UI.
Solution for this particular scenario.
Use animation listeners on your ViewAnimator instances (e.g. ViewPager). To not have to bother with them in teh activity, I would roll your own view, and then use the Adapter type patterns Android is so fond of to manage constantly changing data:
a very hastily written implementation would be:
public class VideoStartingViewFliper extends ViewFlipper {
private final Animation fromRight;
private final Animation toLeft;
private final Animation fromLeft;
private final Animation toRight;
private VideoViewAdapter mAdapter;
public VideoStartingViewFliper(final Context context, final AttributeSet attrs) {
super(context, attrs);
fromRight = new YourChoiceOfAnimation();
fromRight.setAnimationListener(videoStartingAnimationListener);
toLeft = new YourChoiceOfAnimation();
toLeft.setAnimationListener(videoStartingAnimationListener);
fromLeft = new YourChoiceOfAnimation();
fromLeft.setAnimationListener(videoStartingAnimationListener);
toRight = new YourChoiceOfAnimation();
toRight.setAnimationListener(videoStartingAnimationListener);
}
static interface VideoViewAdapter {
public String getVideoPath(int childId);
}
public void setVideoViewAdapter(final VideoViewAdapter adapter) {
mAdapter = adapter;
}
// or even call this showNextVideo and don't override!
#Override
public void showNext() {
setInAnimation(fromRight);
setOutAnimation(toLeft);
super.showNext();
}
#Override
public void showPrevious() {
setInAnimation(fromLeft);
setOutAnimation(toRight);
super.showPrevious();
}
private final AnimationListener videoStartingAnimationListener = new AnimationListener() {
#Override
public void onAnimationStart(final Animation animation) {
final VideoView video = ((VideoView) getCurrentView());
video.stopPlayback();
}
#Override
public void onAnimationRepeat(final Animation animation) {
}
#Override
public void onAnimationEnd(final Animation animation) {
final VideoView video = ((VideoView) getCurrentView());
// check null here!
video.setVideoPath(mAdapter.getVideoPath(getCurrentView().getId()));
video.start();
}
};
}
Hope this helps.
Related
I'm using the TransitionManager.beginDelayedTransition outlined here
to animate two views swapping positions within a RelativeLayout. I do this by simply swapping the RelativeLayout.LayoutParams of two the two views.
My question is how do I monitor the animation that is automatically created and executed by TransitionManager without having to create my own custom Transitions. I need to detect when the animation has ended so that I can make a change to the views that have been swapped.
Below is the method that swaps the two views. CollageCanvasAperture is an extension of View and mApertureGroup is the RelativeLayout that holds these views.
private void shuffle(int fromApertureInd, int toApertureInd) {
final CollageCanvasAperture fromV = (CollageCanvasAperture) mApertureGroup.getChildAt(fromApertureInd);
final CollageCanvasAperture toV = (CollageCanvasAperture) mApertureGroup.getChildAt(toApertureInd);
if (null == fromV || null == toV) {
return;
}
TransitionManager.beginDelayedTransition(mApertureGroup);
RelativeLayout.LayoutParams fromLP = (RelativeLayout.LayoutParams) fromV.getLayoutParams();
RelativeLayout.LayoutParams toLP = (RelativeLayout.LayoutParams) toV.getLayoutParams();
fromV.setLayoutParams(toLP);
toV.setLayoutParams(fromLP);
}
I've done a few hours of searching on here and combing through the TransitionManager code but can't see how to detect changes. I'd prefer to be able to detect the animation end within the CollageCanvasAperture but can't see any relevant listeners to apply.
I guess I could provide the view with the destination LayoutParams before the animation is executed and then the view can listen for size & location changes until they match...?
So it turned out it's quite straightforward to add a listener to these 'automatic' transitions.
Instead of using: TransitionManager.beginDelayedTransition(mApertureGroup);
you need to work out which automatic Transition is being using when you invoke beginDelayedTransition and call TransitionManager.go() instead.
My view transitions were using the ChangeBounds Transition. Once that was know all I had to do was:
ChangeBounds mySwapTransition = new ChangeBounds();
mySwapTransition.addListener(new Transition.TransitionListener() {
#Override
public void onTransitionStart(Transition transition) { }
#Override
public void onTransitionEnd(Transition transition) { }
#Override
public void onTransitionCancel(Transition transition) {}
#Override
public void onTransitionPause(Transition transition) { }
#Override
public void onTransitionResume(Transition transition) { }
});
TransitionManager.go(new Scene(mApertureGroup), mySwapTransition);
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.
I'm working on a game for Android, it uses OpenGL 1.0. I have created a Menu, which was a simple activity with layout, but I didn't liked it, so I decided to create that too in OpenGL, which works, but I don't know how to switch to the actual game. I would like to do it in another GLSurfaceView, because creating everything in one then I must load all textures at the start which could be slow.
My question is that it is possible to change the setContentView or setRenderer somehow?
The basic of the app is like here: http://developer.android.com/resources/tutorials/opengl/opengl-es10.html#creating where setContentView is where I control Touch and Key events, and there I set the setRenderer to GLSurfaceView.
If you have just one activity and one GLSurfaceView, you can switch what you render by manipulating the renderer object.
public class MyRenderer implements Renderer {
Vector<String> modelsToLoad;
HashMap<String, Model> models;
String[] modelsToDraw;
Context context;
#Override
public void onDrawFrame(GL10 gl) {
// load models ahead of time
while(modelsToLoad.size()>0){
String modelFilename = modelsToLoad.remove(0);
models.put(modelFilename, new Model(modelFilename,context,gl));
}
// keep drawing current models
for(int i = 0;i<modelsToDraw.length;i++){
models.get(modelsToDraw[i]).draw(gl);
}
}
// queue models to be loaded when onDraw is called
public void loadModel(String filename){
modelsToLoad.add(filename);
}
// switch to in-game scene
public void drawGame(){
modelsToDraw = new String[]{"tank.mdl", "soldier.mdl"};
}
// switch to menu scene
public void drawMenuBackground(){
modelsToDraw = new String[]{"bouncingBall.mdl", "gun.mdl"};
}
}
Then in onCreate:
MyRenderer myRenderer;
public void onCreate(Bundle bundle){
super.onCreate(bundle);
// set layout which has everything in it
setContentView(R.layout.main);
myRenderer = new Renderer(this);
// load menu models
myRenderer.loadModel("bouncingBall.mdl");
myRenderer.loadModel("gun.mdl");
// set up the glsurfaceview
GLSurfaceView mGLView = findViewById(R.id.glsurfaceview1);
mGLView.setRenderer(myRenderer);
// set the renderer to draw menu background objects
myRenderer.drawMenuBackground();
// set the new game button to start the game
ImageButton newGameButton = findViewById(R.id.new_game_button1);
newGameButton.setOnClickListener(new OnClickListener(){
public void onClick(View v){
// make menu invisible
findViewById(R.id.menu_linearLayout1).setVisibility(View.GONE);
// tell renderer to render game scene
myRenderer.drawGame();
}
});
// make the menu visible
findViewById(R.id.menu_linearLayout1).setVisibility(View.VISIBLE);
// finally we have some time whilst the user decides on their menu option
// use it to load game models in anticipation of the user clicking new game
myRenderer.loadModel("tank.mdl");
myRenderer.loadModel("soldier.mdl");
}
So rather than messing around with two renderer objects or multiple GLSurfaceViews, you instead have a single renderer object, and you just tell it what to render and when. You can manage it so that it loads models and textures only when you need it to or in anticipation of some need. Also it makes things easier if you decide to use the same model in more than one place. If you wanted to put a model in your menu that also features in game, you can just load it once and reuse as many times as you like!
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()
});
}
}
}
I love the API Demo examples from the Android webpage and used the AnimateDrawable.java to
get started with a WaterfallView with several straight falling images which works great. Now I like the images to stop when they are clicked. I found out that Drawables can't handle events so I changed AnimateDrawable and ProxyDrawable to be extended from View instead and added a Click-Event-Listener and Handler on the parent WaterfallView. The animation still works great, but the handler doesn't, probably because in AnimateDrawable the whole canvas is shifted when the drawabled are animated. How can I change that example so that I can implement an event handler? Or is there a way to find out where exactly my AnimateDrawables are in the view?
So the more general question is: How to add an Event Listener / Handler to an animated View?
Here are my changes to the example above:
AnimateView and ProxyView instead of AnimateDrawable and ProxyDrawable
ProxyView extended from View and all super calls changed to mProxy
I commented out mutate()
The context is still the main Activity which is passed down in the constructors
In the constructors of AnimateView setClickable(true) and setFocusable(true) are called
And here is the important source code of the parent/main WaterfallView:
public class WaterfallView extends View implements OnClickListener {
private Context mContext;
// PictureEntry is just a value object to manage the pictures
private Vector<PictureEntry> pictures = new Vector<PictureEntry>();
public WaterfallView(Context context) {
super(context);
mContext = context;
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_0)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_1)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_2)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_3)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_4)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_5)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_6)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_7)));
}
#Override
protected void onDraw(Canvas canvas) {
if(!setup) {
for(PictureEntry pic : pictures) pic.setAnimation(createAnimation(pic));
setup = true;
}
canvas.drawColor(Color.BLACK);
for(PictureEntry pic : pictures) pic.getAnimateView().draw(canvas);
invalidate();
}
private Animation createAnimation(PictureEntry picture) {
Drawable dr = picture.getDrawable();
dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
Animation an = new TranslateAnimation(0, 0, -1*dr.getIntrinsicHeight(), this.getHeight());
an.setRepeatCount(-1);
an.initialize(10, 10, 10, 10);
AnimateView av = new AnimateView(mContext, dr, an);
av.setOnClickListener(this);
picture.setAnimateView(av);
an.startNow();
return an;
}
#Override
public void onClick(View v) {
Log.i("MyLog", "clicked "+v);
}
}
Are you going to be clicking widgets(buttons, checkboxes, etc)? Or do you want to be able to click anywhere? I think you want the latter. So in that case you'll need this method:
#Override
public boolean onTouchEvent(MotionEvent event) {
// do stuff with your event
}
This method is ONLY called when the event is NOT handled by a view, so I think you may have to remove some of your onClickListener stuff. Refer to here for more info. And as always, experiment.