Background
I am working on an implementation of the "KenBurns effect" (demo here) on the action bar , as shown on this library's sample (except for the icon that moves, which I've done so myself).
In fact, I even asked about it a long time ago (here), which at this point I didn't even know its name. I was sure I've found a solution, but it has some problems.
Also, since I sometimes show the images from the device, some of them even need to be rotated, so I use a rotatableDrawable (as shown here).
The problem
The current implementation cannot handle multiple bitmaps that are given dynamically (from the Internet, for example), and doesn't even look at the input images' size.
Instead, it just does the zooming and translation in a random way, so many times it can zoom too much/little, and empty spaces can be shown.
The code
Here's the code that is related to the problems:
private float pickScale() {
return MIN_SCALE_FACTOR + this.random.nextFloat() * (MAX_SCALE_FACTOR - MIN_SCALE_FACTOR);
}
private float pickTranslation(final int value, final float ratio) {
return value * (ratio - 1.0f) * (this.random.nextFloat() - 0.5f);
}
public void animate(final ImageView view) {
final float fromScale = pickScale();
final float toScale = pickScale();
final float fromTranslationX = pickTranslation(view.getWidth(), fromScale);
final float fromTranslationY = pickTranslation(view.getHeight(), fromScale);
final float toTranslationX = pickTranslation(view.getWidth(), toScale);
final float toTranslationY = pickTranslation(view.getHeight(), toScale);
start(view, KenBurnsView.DELAY_BETWEEN_IMAGE_SWAPPING_IN_MS, fromScale, toScale, fromTranslationX,
fromTranslationY, toTranslationX, toTranslationY);
}
And here's the part of the animation itself, which animates the current ImageView:
private void start(View view, long duration, float fromScale, float toScale, float fromTranslationX, float fromTranslationY, float toTranslationX, float toTranslationY) {
view.setScaleX(fromScale);
view.setScaleY(fromScale);
view.setTranslationX(fromTranslationX);
view.setTranslationY(fromTranslationY);
ViewPropertyAnimator propertyAnimator = view.animate().translationX(toTranslationX).translationY(toTranslationY).scaleX(toScale).scaleY(toScale).setDuration(duration);
propertyAnimator.start();
}
As you can see, this doesn't look at the view/bitmap sizes, and just randomly selects how to zoom and pan.
What I've tried
I've made it work with dynamic bitmaps, but I don't understand what to change on it so that it will handle the sizes correctly.
I've also noticed there is another library (here) that does this work, but it also has the same problems, and it's even harder to understand how to fix them there. Plus it randomly crashes . Here's a post I've reported about it.
The question
What should be done in order to implement Ken-Burns effect correctly, so that it could handle dynamically created bitmaps?
I'm thinking that maybe the best solution is to customize the way the ImageView draws its content, so that at any given time, it will show a part of the bitmap that is given to it, and the real animation would be between two rectangles of the bitmap . Sadly, I'm not sure how to do this.
Again, the question isn't about getting bitmaps or decoding. It's about how to make them work well with this effect without crashes or weird zoom in/out which show empty spaces.
I have look at the source code of the KenBurnsView and it isn't actually that hard to implement the features you want, but there are a few things I have to clarify first:
1. Loading images dynamically
The current implementation cannot handle multiple bitmaps that are
given dynamically (from the Internet, for example),...
It isn't difficult to download images dynamically from the internet if you know what you are doing, but there are many ways to do it. Many people don't actually come up with their own solution but use a networking library like Volley to download the image or they go straight for Picasso or something similar. Personally I mostly use my own set of helper classes but you have to decide how exactly you want to download the images. Using a library like Picasso is most likely the best solution for you. My code samples in this answer will use the Picasso library, here is a quick example of how to use Picasso:
Picasso.with(context).load("http://foo.com/bar.png").into(imageView);
2. Image Size
...and doesn't even look at the input images' size.
I really don't understand what you mean by that. Internally the KenBurnsView uses ImageViews to display the images. They take care of properly scaling and displaying the image and they most certainly take the size of the images into account. I think your confusion might be caused by the scaleType which is set for the ImageViews. If you look at the layout file R.layout.view_kenburns which contains the layout of the KenBurnsView you see this:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/image0"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="#+id/image1"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Notice that there are two ImageViews instead of just one to create the crossfade effect. The important part is this tag which is found on both ImageViews:
android:scaleType="centerCrop"
What this does is tell the ImageView to:
Center the image inside the ImageView
Scale the image so its width fits inside the ImageView
If the image is taller than the ImageView it will be cropped to the size of the ImageView
So in its current state the images inside the KenBurnsView may be cropped at all times. If you want the image to scale to fit completely inside the ImageView so nothing has to be cropped or removed you need to change the scaleType to one of those two:
android:scaleType="fitCenter"
android:scaleType="centerInside"
I don't remember the exact difference between those two, but they should both have the desired effect of scaling the image so it fits both on the X and Y axis inside the ImageView while at the same time centering it inside the ImageView.
IMPORTANT: Changing the scaleType potentially messes up the KenBurnsView!
If you really just use the KenBurnsView to display two images then changing the scaleType won't matter aside from how the images are displayed, but if you resize the KenBurnsView - for example in an Animation - and the ImageViews have the scaleType set to something other than centerCrop you will loose the parallax effect! Using centerCrop as scaleType of an ImageView is a quick and easy way to create parallax-like effects. The drawback of this trick is probably what you noticed: The image in the ImageView will most likely be cropped and not completely visible!
If you look at the layout you can see that all Views in there have match_parent as layout_height and layout_width. This could also be a problem for certain images as the match_parent constraint and certain scaleTypes sometimes produce strange results when the images are considerably smaller or larger than the ImageView.
The translate animation also takes the size of the image into account - or at least the size of the ImageView. If you look at the source code of animate(...) and pickTranslation(...) you will see this:
// The value which is passed to pickTranslation() is the size of the View!
private float pickTranslation(final int value, final float ratio) {
return value * (ratio - 1.0f) * (this.random.nextFloat() - 0.5f);
}
public void animate(final ImageView view) {
final float fromScale = pickScale();
final float toScale = pickScale();
// Depending on the axis either the width or the height is passed to pickTranslation()
final float fromTranslationX = pickTranslation(view.getWidth(), fromScale);
final float fromTranslationY = pickTranslation(view.getHeight(), fromScale);
final float toTranslationX = pickTranslation(view.getWidth(), toScale);
final float toTranslationY = pickTranslation(view.getHeight(), toScale);
start(view, KenBurnsView.DELAY_BETWEEN_IMAGE_SWAPPING_IN_MS, fromScale, toScale, fromTranslationX, fromTranslationY, toTranslationX, toTranslationY);
}
So the view already accounts for the images size and how much the image is scaled when calculating the translation. So the concept of how this works is okay, the only problem I see is that both the start and end values are randomised without any dependencies between those two values. What this means is one simple thing: The start and endpoint of the animation might be the exact same position or may be very close to each other. As a result of that the animation may sometimes be very significant and other times barely noticeable at all.
I can think of three main ways to fix that:
Instead of randomising both start and end values you just randomise
the start values and pick the end values based on the start values.
You keep the current strategy of randomising all values, but you impose range restrictions on each value. For example the fromScale should be a random value between 1.2f and 1.4f and toScale should be a random value between 1.6f and 1.8f.
Implement a fixed translation and scale animation (In other words the boring way).
Whether you choose approach #1 or #2 you are going to need this method:
// Returns a random value between min and max
private float randomRange(float min, float max) {
return random.nextFloat() * (max - min) + max;
}
Here I have modified the animate() method to force a certain distance between start and end points of the animation:
public void animate(View view) {
final float fromScale = randomRange(1.2f, 1.4f);
final float fromTranslationX = pickTranslation(view.getWidth(), fromScale);
final float fromTranslationY = pickTranslation(view.getHeight(), fromScale);
final float toScale = randomRange(1.6f, 1.8f);
final float toTranslationX = pickTranslation(view.getWidth(), toScale);
final float toTranslationY = pickTranslation(view.getHeight(), toScale);
start(view, this.mSwapMs, fromScale, toScale, fromTranslationX, fromTranslationY, toTranslationX, toTranslationY);
}
As you can see I only need to modify how fromScale and toScale are calculated because the translations values are calculated from the scale values. This is not a 100% fix, but it is a big improvement.
3. Solution #1: Fixing KenBurnsView
(Use solution #2 if possible)
To fix the KenBurnsView you can implement the suggestions I mentioned above. Additionally we need to implement a way for the images to be added dynamically. The implementation of how the KenBurnsView handles images is a little weird. We are going to need to modify that a bit. Since we are using Picasso this is actually going to be pretty simple:
Essentially you just need to modify the swapImage() method, I tested it like this and it is working:
private void swapImage() {
if (this.urlList.size() > 0) {
if(mActiveImageIndex == -1) {
mActiveImageIndex = 1;
animate(mImageViews[mActiveImageIndex]);
return;
}
final int inactiveIndex = mActiveImageIndex;
mActiveImageIndex = (1 + mActiveImageIndex) % mImageViews.length;
Log.d(TAG, "new active=" + mActiveImageIndex);
String url = this.urlList.get(this.urlIndex++);
this.urlIndex = this.urlIndex % this.urlList.size();
final ImageView activeImageView = mImageViews[mActiveImageIndex];
activeImageView.setAlpha(0.0f);
Picasso.with(this.context).load(url).into(activeImageView, new Callback() {
#Override
public void onSuccess() {
ImageView inactiveImageView = mImageViews[inactiveIndex];
animate(activeImageView);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(mFadeInOutMs);
animatorSet.playTogether(
ObjectAnimator.ofFloat(inactiveImageView, "alpha", 1.0f, 0.0f),
ObjectAnimator.ofFloat(activeImageView, "alpha", 0.0f, 1.0f)
);
animatorSet.start();
}
#Override
public void onError() {
Log.i(LOG_TAG, "Could not download next image");
}
});
}
}
I have omitted a few trivial parts, urlList is just a List<String> which contains all the urls to the images we want to display, urlIndex is used to cycle through the urlList. I moved the animation into the Callback. That way the image will be downloaded in the background and as soon as the image has been downloaded successfully the animations will play and the ImageViews will crossfade. A lot of the old code from the KenBurnsView can now be deleted, for example the methods setResourceIds() or fillImageViews() are now unnecessary.
4. Solution #2: Better KenBurnsView + Picasso
The second library you link to, this one, actually contains a MUCH better KenBurnsView. The KenBurnsView I talk about above is a subclass of FrameLayout and there are a few problems with the approach this View takes. The KenBurnsView from the second library is a subclass of ImageView, this is already a huge improvement. Because of it we can use image loader libraries like Picasso directly on the KenBurnsView and we don't have to take care of anything ourselves. You say that you experience random crashes with the second library? I have been testing it rather extensively the last few hours and didn't encounter a single crash.
With the KenBurnsView from the second library and Picasso this all becomes very easy and very few lines of code, you just have to create a KenBurnsView for example in xml:
<com.flaviofaria.kenburnsview.KenBurnsView
android:id="#+id/kbvExample"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="#drawable/image" />
And then in your Fragment you first have to find the view in the layout and then in onViewCreated() we load the image with Picasso:
private KenBurnsView kbvExample;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_kenburns_test, container, false);
this.kbvExample = (KenBurnsView) view.findViewById(R.id.kbvExample);
return view;
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Picasso.with(getActivity()).load(IMAGE_URL).into(this.kbvExample);
}
5. Testing
I tested everything on my Nexus 5 running Android 4.4.2. Since ViewPropertyAnimators are used this should all be compatible somewhere down to API Level 16, maybe even 12.
I have a omitted a few lines of code here and there so if you have any questions feel free to ask!
Related
How can I create circular list for round watch as in android wear 2.0 ?.
Like this:
Circular list is seen in android wear app launcher.
First of all you need to replace your ListView with a WearableRecyclerView.
It can be used like a normal ListView. But make sure, you import the right one from android.support.wear.widget. DON'T use the one from android.support.wearable.view. This one should be crossed out, so it won't take you long to check if you're using the right one. If there's just one WearableRecyclerViewto choose from, make sure to add compile 'com.android.support:wear:27.0.0' to the dependencies in your build.gradle (wear) file.
Also make sure that you're using <android.support.wear.widget.WearableRecyclerView/> in your activity.xml. If you just want a circular ListView without any custom item-scaling, just call this in your onLayoutInflated() method:
your_recyclerview.setEdgeItemsCenteringEnabled(true);
your_recyclerview.setLayoutManager(new WearableLinearLayoutManager(your_activity_context));
If you want to make items scaling up when they get closer to the center of your screen, things get a little more complicated.
First: paste this in your Activity.java:
private class CustomScrollingLayoutCallback extends WearableLinearLayoutManager.LayoutCallback {
private static final float MAX_ICON_PROGRESS = 2F;
#Override
public void onLayoutFinished(View child, RecyclerView parent) {
float centerOffset = ((float) child.getHeight() / 2.0f) / (float) parent.getHeight();
float yRelativeToCenterOffset = (child.getY() / parent.getHeight()) + centerOffset;
float progresstoCenter = (float) Math.sin(yRelativeToCenterOffset * Math.PI);
float mProgressToCenter = Math.abs(0.5f - yRelativeToCenterOffset);
mProgressToCenter = Math.min(mProgressToCenter, MAX_ICON_PROGRESS);
child.setScaleX(1 - mProgressToCenter);
child.setScaleY(1 - mProgressToCenter);
child.setX(+(1 - progresstoCenter) * 100);
}
}
Then go back to your onLayoutInflated() method, and type the following:
CustomScrollingLayoutCallback customScrollingLayoutCallback = new CustomScrollingLayoutCallback();
your_recycler_view.setLayoutManager(new WearableLinearLayoutManager(your_context, customScrollingLayoutCallback));
your_recycler_view.setCircularScrollingGestureEnabled(true);
done.
This is now possible with Android Wear 2.0's WearableRecyclerView.
According to Android Developer Docs:
Wear 2.0 introduces the WearableRecyclerView class for displaying and
manipulating a vertical list of items optimized for round displays.
WearableRecyclerView extends the existing RecyclerView class to
provide a curved layout and a circular scrolling gesture in wearable
apps.
You may like to read more about Android Wear 2.0 Preview 3.
I have a ViewPager which I need to move as a whole on button press. I use an animation for this.
When I press it, I translate the 'x' for it. I use setFillAfter(true) to keep the new position.
But when I change the page of the ViewPager, it jumps back to the original x-position!
I only saw this issue on Android 4.1, with Android 4.0 there is no problem! So it looks like some kind of regression in Android.
I attached a testproject where I could reproduce the issue without all my other stuff around it. I think it is best if you want to help me figure this out to import the project in your Eclipse and see it for yourself.
I also added to video's, one on my HTC One X where I see the issue, and the other on a tablet with Android 4.0, where the issue is not there.
I have been desperately looking to fix this ugly side effect, but no luck till now...
(Sorry for the big movie files...)
Video of Android 4.0 without the side effect
Video Android 4.1 with the side effect
the project where you can reproduce the issue with
Edit:
I added the following:
#Override
public void onAnimationEnd(Animation animation) {
RelativeLayout.LayoutParams lp = (android.widget.RelativeLayout.LayoutParams) myViewPager.getLayoutParams();
if (!i)
lp.setMargins(300,0,0,0);
else
lp.setMargins(0,0,0,0);
myViewPager.setLayoutParams(lp);
}
After that it stays at the correct position, but it 'flickers' quickly, like the animation is still showing at the end and when I change the margin, it still shows the offset it had after animation. Then it jumps to the correct position.
The main problem seems to be incorrect choice of animation type. You see, View Animation as a tool is not intended to be used with complex interactive objects like ViewPager. It offers only low-cost animation of the drawing place of views. The visual behaivior of the animated ViewPager in response to user-actions is undefined and should not be relied on.
Ugly flicks, when you substitute a "gost" with the real object are only natural.
The mechanism, that is intended to use in your case since API 11 is specialized property animator built in Views for optimized performance: ViewPropertyAnimator, or not specialized, but more versatile ObjectAnimator and AnimatorSet.
Property animation makes the View to really change its place and function normally there.
To make project, to use, say, ViewPropertyAnimator, change your listener setting to this:
btn.setOnClickListener(new OnClickListener() {
boolean b = false;
#Override
public void onClick(View v) {
if(b) {
myViewPager.animate().translationX(0f).setDuration(700);
}
else {
myViewPager.animate().translationX(300f).setDuration(700);
}
b=!b;
}
});
If you want to use xml configuration only, stick to |ObjectAnimator and AnimatorSet. Read through the above link for further information.
In case, you are anxious to support pre-Honeycomb devices, you can use Jake Warton's NineOldAndroids project. Hope that helps.
That's because the Animation's setFillAfter(true) doesn't actually change the position or any attributes of the View; all it does is create a Bitmap of the view's drawing cache and leaves it where the animation ends. Once the screen is invalidated again (ie. changing the page in the ViewPager), the bitmap will be removed and it will appear as if the View is returning to it's original position, when in fact it was already there.
If you want the View to retain it's position after the animation has finished, you need to actually adjust the View's LayoutParams to match your desired effect. To achieve this, you can override the onAnimationEnd method of the Animation, and adjust the LayoutParams of the View inside there.
Once you adjust the LayoutParams, you can remove your call to setFillAfter(true) and your View will actually stay where you expect it to.
Regarding the flicker issue:
I have encountered this issue before, and it stems from the possibility of the onAnimationEnd() call not syncing up with the next layout pass. Animation works by applying a transformation to a View, drawing it relative to its current position.
However, it is possible for a View to be rendered after you have moved it in your onAnimationEnd() method. In this case, the Animation's transformation is still being applied correctly, but the Animation thinks the View has not changed its original position, which means it will be drawn relative to its ENDING position instead of its STARTING position.
My solution was to create a custom subclass of Animation and add a method, changeYOffset(int change), which modifies the y translation that is applied during the Animation's applyTransformation method. I call this new method in my View's onLayout() method, and pass the new y-offset.
Here is some of my code from my Animation, MenuAnimation:
/**
* Signal to this animation that a layout pass has caused the View on which this animation is
* running to have its "top" coordinate changed.
*
* #param change
* the difference in pixels
*/
public void changeYOffset(int change) {
fromY -= change;
toY -= change;
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float reverseTime = 1f - interpolatedTime;
float dy = (interpolatedTime * toY) + (reverseTime * fromY);
float alpha = (interpolatedTime * toAlpha) + (reverseTime * fromAlpha);
if (alpha > 1f) {
alpha = 1f;
}
else if (alpha < 0f) {
alpha = 0f;
}
t.setAlpha(alpha);
t.getMatrix().setTranslate(0f, dy);
}
And from the View class:
private int lastTop;
// ...
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// the animation is expecting that its View will not be moved by the container
// during its time period. if this does happen, we need to inform it of the change.
Animation anim = getAnimation();
if (anim != null && anim instanceof MenuAnimation) {
MenuAnimation animation = (MenuAnimation) anim;
animation.changeYOffset(top - lastTop);
}
// ...
lastTop = top;
super.onLayout(changed, left, top, right, bottom);
}
Crucero has it right about setFillAfter not adjusting params post invalidation. When the view is re-layed out (which'll happen the pass after it's invalidated), its layout params will be the ones that always applied, so it should go back to the original position.
And Jschools is right about onAnimationEnd. Strongly encourage you to step through the source code with a debugger, where you'll instructively discover that an update is made that affects the drawn position of the view after onAnimationEnd is fired, at which point you've actually applied the layout params, hence the flicker caused by doubled up offset.
But this can be solved quite simply by making sure you relayout at the right time. You want to put your re-positioning logic at the end of the ui message queue at the time of animation end so that it is polled after the animation but before laying out. There's nowhere that suggests doing this, annoyingly, but I've yet find a reason in any release of the SDK reason why (when doing this just once and not incorrectly using ui thread) this shouldn't work.
Also clear the animation due to another issue we found on some older devices.
So, try:
#Override
public void onAnimationEnd(final Animation animation) {
myViewPager.post(new Runnable() {
#Override
public public void run() {
final RelativeLayout.LayoutParams lp = (android.widget.RelativeLayout.LayoutParams) myViewPager.getLayoutParams();
if (!someBooleanIPresume)
lp.setMargins(300,0,0,0);
else
lp.setMargins(0,0,0,0);
myViewPager.setLayoutParams(lp);
myViewPager.clearAnimation();
}
}
I'm a little bit stuck with this one - first and foremost, the following link has been useful however I've come up with a bit of an issue with visibility:
The link: Check view visibility
I have a scroll view (parent) and a number of sub-views (LinearLayout -> TableLayout) etc. There are a number of items I set to View.GONE within the XML (android:visibility="gone").
I have some simple code to determine whether it is visible or not using getVisibility() however when I set the item to View.VISIBLE and try to immediately getDrawingRect() I get a Rect with zeros across the board. Any further click gets the correct coordinates.
Now this could be because the view has never been drawn (as defined in the XML) causing it to return no coordinates however I do set View.VISIBLE before trying to determine screen visibility. Could it be that I need to get some kind of callback from say the onDraw()? or perhaps set the view visibility of hidden items within code. A bit annoying ;(
Some code:
Rect scrollBounds = new Rect();
scroll.getHitRect(scrollBounds);
Rect viewBounds = new Rect();
if (view.getVisibility() == View.GONE) {
view.setVisibility(View.VISBLE)
viewBounds.getDrawingRect(viewBounds);
if (!Rect.intersects(scrollBounds, viewBounds) {
// do somthing
}
}
Layouts area as follows:
ScrollView
LinearLayout
TableLayout
Button
HiddenView
Of course, it's highly likely I'm going about this the wrong way altogether - basically I just want to make sure that the scrollview positions itself so the view that has become visible can be seen in it's entirety.
If any other information is required, let me know!
Ok so thanks to OceanLife for pointing me in the right direction! There was indeed a callback required and ViewTreeObserver.OnGlobalLayoutListener() did the trick. I ended up implementing the listener against my fragment class and picked it up where I needed it. Thanks for the warning too regarding the multiple calls, I resolved this using the removeOnGlobalLayoutListener() method - works a charm.
Code:
...
// vto initialised in my onCreateView() method
vto = getView().getViewTreeObserver();
vto.addOnGlobalLayoutListener(this);
...
#Override
public void onGlobalLayout() {
final int i[] = new int[2];
final Rect scrollBounds = new Rect();
sView.getHitRect(scrollBounds);
tempView.getLocationOnScreen(i);
if (i[1] >= scrollBounds.bottom) {
sView.post(new Runnable() {
#Override
public void run() {
sView.smoothScrollTo(0, sView.getScrollY() + (i[1] - scrollBounds.bottom));
}
});
}
vto.removeOnGlobalLayoutListener(this);
}
Just got to do some cleaning up now ...
So, if I am reading this right the issue you are having is that you want to find out the dimensions of some view in your layout to find out whether you have an intersection with the parent ScrollView.
What I think you are seeing (as you alluded to) is the instruction to draw the view being dispatched, and then in some sort of race-condition, the renderer resolving the measurements for the layout and the actual render where view objects get real sizes. One way to find out what sort of dimensions a view has on screen is to use the layout tree-listener. We use this observer to resolve a screen's dimensions when leveraging the Google Charts API, which requires a pixel width and height to be defined... which of course on Android is probably the biggest problem facing developers. So observe (pun intended);
final ViewTreeObserver vto = chart.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
/**
* #see android.view.ViewTreeObserver.OnGlobalLayoutListener#onGlobalLayout()
*/
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
if (view.getVisibility() == View.GONE) {
view.setVisibility(View.VISBLE)
viewBounds.getDrawingRect(viewBounds);
if (!Rect.intersects(scrollBounds, viewBounds) {
// do something
}
}
}
});
Word of warning the onGlobalLayout will get called multiple times as the renderer closes in on the final solution. The last call is the one we take.
This will provide you with a mechanism for performing actions on a drawn view like getting a view component's width and height. Hope that helps you out.
Aside: The clever chaps over at Square have designed a FEST hamcrest library that will allow you to write a test for this alongside Robotium. Check it out.
i'm trying out libgdx as an opengl wrapper , and i have some issues with its graphical rendering :
for some reason , all images (textures) on android device look a little blurred using libgdx . this also includes text (font) .
for text , i thought that it's because i use bitmap-fonts , but i can't find an alternative- i've found out that there is a library called "gdx-stb-truetype" , but i can't find how to download it and use it .
for normal images , even when i show the entire image without any scaling , i expect it to look as sharp as i see it on a computer's screen , especially if i have such a good screen on the device (it's galaxy nexus) .
i've tried to set the anti-aliasing off , by using the next code :
final AndroidApplicationConfiguration androidApplicationConfiguration=new AndroidApplicationConfiguration();
androidApplicationConfiguration.numSamples=0; //tried the value of 1 too.
...
i've also tried to set the scaling method to various methods , but with no luck. example:
texture.setFilter(TextureFilter.Nearest,TextureFilter.Nearest);
as a test , i've found a sharp image that is exactly the same as the seen resolution on the device (720x1184 for galaxy nexus , because of the buttons bar) , and i've put it to be on the background of the libgdx app . of course , i had to add extra blank space in order for the texute to be loaded , so the final size of the image (which will include content and empty space) is still a power of 2 for both width and height (1024x2048 in this case) .
on the desktop app , it look ok . on the device , it looked blurred.
a weird thing that i've noticed is that when i change the device's orientation (horizontal <=> vertical) , for the very short time before the rotating animation starts , i see both the image and the text very well .
surely libgdx can handle this , since the opengl part of the api-tests project of android shows images just fine.
can anyone please help me?
#user1130529 : i do use spritebatch . also , here's what i do for setting the viewport . it occurs whether i choose to keep the aspect ratio or not.
public static final int VIRTUAL_WIDTH =720;
public static final int VIRTUAL_HEIGHT =1280-96;
private static final float ASPECT_RATIO =(float)VIRTUAL_WIDTH/(float)VIRTUAL_HEIGHT;
...
#Override
public void resize(final int width,final int height)
{
// calculate new viewport
if(!KEEP_ASPECT_RATIO)
{
_viewport=new Rectangle(0,0,Gdx.app.getGraphics().getWidth(),Gdx.app.getGraphics().getHeight());
Gdx.app.log("DEBUG","size:"+_viewport);
return;
}
final float currentAspectRatio=(float)width/(float)height;
float scale=1f;
final Vector2 crop=new Vector2(0f,0f);
if(currentAspectRatio>ASPECT_RATIO)
{
scale=(float)height/(float)VIRTUAL_HEIGHT;
crop.x=(width-VIRTUAL_WIDTH*scale)/2f;
}
else if(currentAspectRatio<ASPECT_RATIO)
{
scale=(float)width/(float)VIRTUAL_WIDTH;
crop.y=(height-VIRTUAL_HEIGHT*scale)/2f;
}
else scale=(float)width/(float)VIRTUAL_WIDTH;
final float w=VIRTUAL_WIDTH*scale;
final float h=VIRTUAL_HEIGHT*scale;
_viewport=new Rectangle(crop.x,crop.y,w,h);
Gdx.app.log("DEBUG","viewport:"+_viewport+" originalSize:"+VIRTUAL_WIDTH+","+VIRTUAL_HEIGHT+" aspectRatio:"+ASPECT_RATIO+" currentAspectRatio:"+currentAspectRatio);
}
Try this:
TextureRegion.getTexture().setFilter(TextureFilter.Linear, TextureFilter.Linear);
Try the following:
texture.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
There are several types of TextureFilters. I assume that the Linear one (is that default?) is blurring.
If you have the Chainfire 3D application or another which reduce textures or change it to 16bit, turn it off; that works for me, and I had the same problem.
I'm looking for ways to speed up a few animations.
I came across this article that mentions in passing that storing a large bitmap in a static field can help with application load times (see paragraph 4).
What would be the equivalent technique for an AnimationDrawable?
Is it even possible to preload the frames in an animation drawable? Are they preloaded by default? Will this help me speed things up?
I am running a frame animation which is composed of .png's which are 256x256, they are being scaled down to the size of the image view, which I would guess is about 100x100, this will change when the app is used on a device with a different screen size then mine, however the size of this 100x100 image view will not change after the onCreate method of my app is called.
Solution: I modified the accepted solution as follows so that I could use the standard animation format that android uses (and that I already had my animations encoded in):
public Bitmap[] setAnimationArray(int res_anim){
this.my_view.setBackgroundResource(res_anim); //view sized properly elsewhere
AnimationDrawable t_anim = (AnimationDrawable) this.my_view.getBackground();
Bitmap[] anim = new Bitmap[t_anim.getNumberOfFrames()];
for(int i = 0;i<t_anim.getNumberOfFrames();i++){
anim[i]=((BitmapDrawable) t_anim.getFrame(i)).getBitmap(); //extract bitmaps from the animation
}
return anim;
Edit: to the answer below I would like to add that performing animations in a surface view manually seems to be much faster then animating using the standard methods. Search Android SurfaceView for more info.
Define your bitmaps in arrays.xml like so:
<array name="targetFrames">
<item>#drawable/bitmap1</item>
<item>#drawable/bitmap2</item>
...
</array>
You can do something like this to create an array of bitmaps:
private void setupTargetFrames() {
TypedArray targetResources = context.getResources().obtainTypedArray(R.array.targetFrames);
targetFrames = new Bitmap[targetResources.length()];
for (int i = 0; i < targetResources.length(); i++) {
targetFrames[i] = BitmapFactory.decodeResource(context.getResources(), targetResources.getResourceId(i, R.drawable.defaulBitmap));
}
targetResources.recycle();
}
Then play the animation by cycling through the bitmaps.
See BitmapFactory's other methods if you want to scale the bitmap when loading it in.