Animated Vector Drawable using compat library even on API 22 device - java

I wrote an animated vector drawable using path morphing (which is available only on API 21 and above). I have a fallback animation using a simple rotation for API below 21. I'm using the animated vector drawable support library (com.android.support:animated-vector-drawable:25.3.1).
Here is how I start the animation:
mBinding.switchIcon.setImageResource(R.drawable.ic_animated_vertical_arrow_down_to_up_32dp);
final Drawable animation = mBinding.switchIcon.getDrawable();
if (animation instanceof Animatable) {
((Animatable) animation).start();
}
This works fine on API 19 and 24, but doesn't work on API 22 nor 23 (I don't have an API 21 device to test).
The API 19 case is logical: the animation is simple, is handled by the support library perfectly, it works. Great.
I expected any API 21 and above devices to pick the built-in vector drawable implementation. However, when debugging, I can see that animation is in fact an instance of AnimatedVectorDrawableCompat: hence, it doesn't support path morphing, and the animation doesn't work.
So why does it work on API 24? Well, animation is an instance of AnimatedVectorDrawable. Hence, path morphing works fine.
So my question is: why doesn't API 21-23 devices pick up the built-in implementation, and rely on the support library, while an API 24 device does pick it up?
As a side note, forcing the device to pick the built-in implementation does obviously works:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AnimatedVectorDrawable drawable = (AnimatedVectorDrawable) getDrawable(R.drawable.ic_animated_vertical_arrow_down_to_up_32dp);
mBinding.switchIcon.setImageDrawable(drawable);
} else {
mBinding.switchIcon.setImageResource(R.drawable.ic_animated_vertical_arrow_down_to_up_32dp);
}
final Drawable animation = mBinding.switchIcon.getDrawable();
if (animation instanceof Animatable) {
((Animatable) animation).start();
}
I also found this (probably) related issue on the Google bug-tracker: https://issuetracker.google.com/issues/37116940
Using a debugger, I can confirm that on API 22 (and probably 23), the support libraries are indeed delegating the work to the SDK's AnimatorSet. I really don't understand the behavior change.
About what follows
These are some notes I thought could be interesting to share, which I took while investigating in the technical explanation of this issue. The intersting, less technical bits are summarized in the accepted answer.
Here is the AVD I'm using, for reference:
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:alpha="1">
<group android:name="group">
<path
android:name="path"
android:pathData="#string/vertical_arrow_up_path"
android:strokeColor="#000000"
android:strokeWidth="2"
android:strokeLineCap="square"/>
</group>
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="path"
android:propertyName="pathData"
android:duration="300"
android:valueFrom="#string/vertical_arrow_up_path"
android:valueTo="#string/vertical_arrow_down_path"
android:valueType="pathType"
android:interpolator="#android:anim/accelerate_decelerate_interpolator"/>
</aapt:attr>
</target>
</animated-vector>
And both path resources:
<string name="vertical_arrow_up_path" translatable="false">M 7.41 10 L 12 14.585 L 16.59 10</string>
<string name="vertical_arrow_down_path" translatable="false">M 7.41 14.585 L 12 10 L 16.59 14.585</string>
On an API 22 device, both the built-in and the support version (25.3.1) seems to inflate the same Animator from my AVD above, albeit with a different hierarchy.
With the support version (25.3.1), the AnimatorSet has only one node: an AnimatorSet containing itself a single animation, seemingly matching the ObjectAnimator described in the AVD's XML. Its referent is set to the VectorDrawableCompat, the property name is rightfully pathData, and the values list is containing a single PropertyValuesHolder with two keyframes, matching my start and end paths. Result: doesn't work.
With the build-in version (SDK 22), it's not exactly the same (but the AnimatorSet isn't exactly in the same place, so…): in the AnimatedVectorDrawableState, the mAnimators list has 1 element, which is directly the ObjectAnimator (with the same values as with the support version). Result: works.
The only relevant difference I can see is the ValueAnimator in the PropertyValuesHolder. As it has some reference to the drawable, I guess it may have some typecheck ignoring the support library version of the VectorDrawable class. But that's pure guesswork at that point. I'll keep digging…
I finally got it (and accepted #LewisMcGeary's answer, as I didn't mention in this question that I was looking for the technical bits behind the issue). Here's what happens. As mentioned, on APIs 21-23, the support library is taking over the SDK's implementation, to avoid bugs in said implementations. So we're using AnimatedVectorDrawableCompat and other [whatever]Compat classes. Once the vector itself is loaded, it's the animation's turn.
The animation is delegated to the SDK's ObjectAnimator, whatever the API level we're on (at least on 21+, but I guess it's the same thing on 19 and below). To animate primitive types, the ObjectAnimator has an internal map of functions to call to change the values. However, on complex types, it's relying on a specific method signature which has to be present on the animated object. Here's the method mapping value type to corresponding method to call, from PropertyValuesHolder (SDK, API 22):
private Method getPropertyFunction(Class targetClass, String prefix, Class valueType) {
// TODO: faster implementation...
Method returnVal = null;
String methodName = getMethodName(prefix, mPropertyName);
Class args[] = null;
if (valueType == null) {
try {
returnVal = targetClass.getMethod(methodName, args);
} catch (NoSuchMethodException e) {
// Swallow the error, log it later
}
} else {
args = new Class[1];
Class typeVariants[];
if (valueType.equals(Float.class)) {
typeVariants = FLOAT_VARIANTS;
} else if (valueType.equals(Integer.class)) {
typeVariants = INTEGER_VARIANTS;
} else if (valueType.equals(Double.class)) {
typeVariants = DOUBLE_VARIANTS;
} else {
typeVariants = new Class[1];
typeVariants[0] = valueType;
}
for (Class typeVariant : typeVariants) {
args[0] = typeVariant;
try {
returnVal = targetClass.getMethod(methodName, args);
if (mConverter == null) {
// change the value type to suit
mValueType = typeVariant;
}
return returnVal;
} catch (NoSuchMethodException e) {
// Swallow the error and keep trying other variants
}
}
// If we got here, then no appropriate function was found
}
if (returnVal == null) {
Log.w("PropertyValuesHolder", "Method " +
getMethodName(prefix, mPropertyName) + "() with type " + valueType +
" not found on target class " + targetClass);
}
return returnVal;
}
The interesting part is the for loop trying to match any potential typeVariants to our target class. On this specific case, typeVariants contains only one Class object: android.util.PathParser$PathDataNode. The class we're trying to call a method on (targetClass) is our Compat class: android.support.graphics.drawable.VectorDrawableCompat$VFullPath. And the method we're looking for (methodName) is setPathData.
Sadly, VectorDrawableCompat$VFullPath.setPathData's signature doesn't match: public void android.support.graphics.drawable.VectorDrawableCompat$VPath.setPathData(android.support.graphics.drawable.PathParser$PathDataNode[])
As we only have one item in the typeVariants array, returnVal ends being null, and in the end, the ObjectAnimator has absolutely no way to know how to update the path data of our VectorDrawableCompat.
So from where comes the typeVariants content? The android.util.PathParser$PathDataNode instead of the support one? It's because of the way the animation is inflated. AnimatedVectorDrawableCompat, as we saw, is delegating much of the work to the SDK, which is why some things doesn't work on APIs 19 and below. When reading the target node of its XML, the Animator is inflated by the SDK:
Animator objectAnimator = AnimatorInflater.loadAnimator(mContext, id);
The AnimatorInflater comes from the SDK, and is hence inflating a android.util.PathParser$PathDataNode instead of a android.support.graphics.drawable.PathParser$PathDataNode. I guess the only possible fix for this would be for Google to integrate the AnimatorInflater in the support libraries…
So we're in a hard position here. Google admits that the VectorDrawable implementation from SDKs 21-23 contains bugs (I noticed some drawing issues on API 22 on some SVGs), but we can't use everything from the support library either. So, keep in mind that testing on 19 (or below), 21, 22, 23 and 24 (or above) is just mandatory when it comes to VectorDrawables…
Edit: as of today (09/06/2017), Google released support libraries 25.4, which back-ports path-morphing on API 14+. I guess this issue is now automatically solved (I didn't tested it yet).

AnimatedVectorDrawableCompat does a version check internally and delegates to the system implementation only if the version is API 24 or above (at time of writing).
As for the reasoning, it seems to be as mentioned in the issue you linked to, to avoid problems with the built in implementation for earlier APIs.
For the most recent one, here's the git commit, which refers to this issue in the issue tracker about rendering problems.
Unfortunately that does mean that fixing some things also removes other features (eg. path morphing). I think the type of approach you use in the question is really the only option at present to get around this.

Related

HERE Api: How do I request PDE data for Safety_Alerts layer without getting Bad Request?

I am creating an Android App that accesses the HERE Platform Data Extension Api (= PDE). Therefore I first load a map centering on my current location. This works fine so far. Then I try to load data from the PDE for the layer "SAFETY_ALERTS", but I get a 400 Error there with the message "tilexy lists 992 tiles but the limit is 64 tiles".
I am not sure where this "tilexy" comes from. I already researched through as much documentation of the PDE as I could find online, but I couldn't find an answer.
Set<String> layers = new HashSet<>(Arrays.asList("SAFETY_ALERTS"));
GeoBoundingBox bbox = map.getBoundingBox();
final PlatformDataRequest request = PlatformDataRequest.createBoundingBoxRequest(layers, bbox);
request.execute(new PlatformDataRequest.Listener<PlatformDataResult>() {
#Override
public void onCompleted(PlatformDataResult platformDataResult, PlatformDataRequest.Error error) {
if (error == null) {
//do something
} else {
//show error --> Here is where I get
}
I expected to get a PlatformDataItemCollection, which is a List of PlatformDataItems (they implement Map). Instead I got the 400-Error.
Does somebody know where this error comes from and can help me fix my mistake?
As per the error message, it would be advisable to check the API call as it seems that more than 64 coordinates has been passed in tilexy parameter in the rest call. tilexy is a string, which is passed in comma separated sequence of tilex,tiley pairs for the requested tiles. The tilex and tiley values are described in the "tile" resource.
please refer following documentation for more reference
developer.here.com/documentation/platform-data/topics/example-tiles.html
Happy Coding..!!

Correct way to check if JavaFX MediaPlayer can play a file

Currently, this is how I check if a file is playable through the JavaFX MediaPlayer, since it's the way it is done internally.
import static com.sun.media.jfxmedia.MediaManager.canPlayContentType;
import static com.sun.media.jfxmediaimpl.MediaUtils.filenameToContentType;
public boolean isPlayable(String filename) {
return canPlayContentType(filenameToContentType(filename));
}
the problem is that the packages that contain these Methods "are not API" and not accessible on Java 9 anymore. I know that there are workarounds, but I wonder if there is an actually correct, future-proof way of doing this?
I want this method to populate a Library with all the playable content within a directory:
Files.find(folderPath, Integer.MAX_VALUE, (path, attr) ->
attr.isRegularFile() && isPlayable(path.toFile().getName()))
.forEach(path -> addSong(path));
I went through the documentation of javafx.media module for the sake of finding any such built-in API and was unable to find one.
A look at the existing Implementation of filenameToContentType(String filename), which is somewhat:-
if (extension.equals(FILE_TYPE_AIF) || extension.equals(FILE_TYPE_AIFF)) {
contentType = CONTENT_TYPE_AIFF;
} else if ... other content types
That eventually checks if the current file extension is one of the supported container type and returns the content types based on the same.
The other piece on the board crucially was canPlayContentType(String contentType) which seems to be relying eventually on the supportedContentTypes for each platform as defined in the NativeMediaManager class.
Though I haven't tested the solution as proposed below primarily due to unawareness of the overview of the task that you intend to perform eventually. Yet, the closest to your current implementation and what Basic PlayBack guidelines suggests as well, was to try
Construct a Media instance out of the filename that you were providing.
Check for a MediaException if any while performing (1).
Further, the exception type MediaException.Type MEDIA_UNSUPPORTED states that
Indicates that this media type is not supported by this platform.
Drawing from the analogy with this and your current solution, you could probably make use of the this:
private static boolean isPlayable(String filename) {
try {
Media media = new Media(filename);
} catch (MediaException e) {
if (e.getType() == MediaException.Type.MEDIA_UNSUPPORTED) {
return false;
}
}
return true;
}
PS: Though I believe this could be further optimized if you actually start making use of the Media constructed(as in the above stub) right away in your piece of code instead of just dropping it.

Java Jinput: rescan / reload controllers

I am using java jinput library to read data from joypad, and I have trouble reloading Controllers, I use this to load them:
public Controller[] findStickControllers() {
ControllerEnvironment ce =
ControllerEnvironment.getDefaultEnvironment();
Controller[] cs = ce.getControllers();
System.out.println(cs.length); //test
ArrayList<Controller> sel = new ArrayList<>();
for (Controller c: cs) {
if(c.getType() == Type.STICK) {
sel.add(c);
}
}
return sel.toArray(new Controller[]{});
}
This works fine, but if I disconnect my controller, calling this will find it again, and vice versa (connecting it after the first check will not find it at all).
I have tried to put sleep before the fist lookup, with these results:
Controllers are acctually scanned when this method is called first time (not at start of the program)
When called again, this always returns same controllers as it returned for the first time.
First call will also write warning bellow
Even when controller is connected (and works), then disconnected (it will still find it though) and reconnected, it will not work
Warning from point 3: (didn't format well in the list)
WARNING: Found unknown Windows version: Windows 8
Attempting to use default windows plug-in.
Loading: net.java.games.input.DirectAndRawInputEnvironmentPlugin
I am using Win 8, and had same problem on Win 7. I had also tried this with mouse, same results.
How can I acctually reload controllers for the 2nd, 3rd, and so on time?
I encountered the same problem. The reason is that the actual hardware scan happens only once for each DefaultControllerEnvironment object. Since the only accessible instantiation is a singleton, it never does another scan.
A simple way to force a hardware scan is to create a new object, but neither the class nor the constructor are public. You can however work around this limitation by calling the constructor via reflection.
Rescan
private static ControllerEnvironment createDefaultEnvironment() throws ReflectiveOperationException {
// Find constructor (class is package private, so we can't access it directly)
Constructor<ControllerEnvironment> constructor = (Constructor<ControllerEnvironment>)
Class.forName("net.java.games.input.DefaultControllerEnvironment").getDeclaredConstructors()[0];
// Constructor is package private, so we have to deactivate access control checks
constructor.setAccessible(true);
// Create object with default constructor
return constructor.newInstance();
}
Usage
// Be aware that creating a new environment is fairly expensive
Controller[] controllers = createDefaultEnvironment().getControllers();
Remove Windows 8 Warnings
/**
* Fix windows 8 warnings by defining a working plugin
*/
static {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run() {
String os = System.getProperty("os.name", "").trim();
if (os.startsWith("Windows 8")) { // 8, 8.1 etc.
// disable default plugin lookup
System.setProperty("jinput.useDefaultPlugin", "false");
// set to same as windows 7 (tested for windows 8 and 8.1)
System.setProperty("net.java.games.input.plugins", "net.java.games.input.DirectAndRawInputEnvironmentPlugin");
}
return null;
}
});
}
If you use the accepted answer, you might want to consider killing the thread that was spawned by the previous environment before setting a new one because it won't be cleaned up otherwise. You can do so by calling something like:
final Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
for (final Thread thread : threadSet) {
final String name = thread.getClass().getName();
if (name.equals("net.java.games.input.RawInputEventQueue$QueueThread")) {
thread.interrupt();
try {
thread.join();
} catch (final InterruptedException e) {
thread.interrupt();
}
}
}
The warning is because the last time I updated that code windows 7 wasn't even out IIRC, I'll update it.
The controller reload is a feature that has been requested a number of times, but no-one deems it important enough to spend any time implementing it. If you submit a patch I'll take a look and see about committing it. Until someone finds it important enough to spend the time to write it, it's just a missing feature.
I had the same problem before.
I add the rescanning feature (for Windows back-end only) and post the patch on Java gaming forum but no ones seem interested in to integrate it.
So if you need it, apply my patch from here: http://www.java-gaming.org/topics/rescan-controllers/24782/msg/224604/view.html#msg224604

Possible to only load specific lines of code according to Android OS version?

Is there a simple line of code that would allow only loading the code if the OS version meets the requirements?
Lets say I have my target OS as 2.2 but the min sdk is 3 for android 1.5 so even if i have some code in my project that isn't compatable with 1.5 it will still compile since the target OS is 2.2. Anyway, I want to ad a feature that requires code that's not in the 1.5 SDK and will cause a crash if it's loaded on a 1.5 phone. Is there a simple thing like this that I can do? So i dont have to make the entire app not available to 1.5 users?
if (Android OS == >2.1){
//Insert code here that requires 2.1 and up}
else{
//insert code that would appear is OS is <2.1}
Yes, you can do that. In fact there is more than one way. (Note: the only Android specific part of this answer is the way that you find out the platform version.)
Suppose that class X has method void y() in version 2.0 onwards, but not before.
One way to invoke this method with out introducing any compile time dependencies whatsoever is to use reflection to locate the Method and call invoke on it. For example:
X x = ...
if (BUILD.VERSION.RELEASE.compareTo("2.0") >= 0) {
// (exception handling omitted ...)
Method m = c.getClass().getDeclaredMethod("y");
m.invoke(x);
}
Another way is to create a version compatibility adapter API for your application like this:
/** Version compatibility adapter API */
interface Compat {
void doY();
}
/** Adapter class for version 1 */
class CompatV1 {
public void y(X x) {
// do nothing
}
}
/** Adapter class for version 2 */
class CompatV2 {
public void y(X x) {
x.y();
}
}
//
// Code to instantiate the relevant adapter for the current platform.
//
Class<?> compatClass;
// (Exception handling omitted)
if (BUILD.VERSION.RELEASE.compareTo("2.0") < 0) {
compatClass = Class.forName("...CompatV1");
} else {
compatClass = Class.forName("...CompatV2");
}
// (Exception handling omitted)
Compat compat = (Compat) compatClass.newInstance();
// The adapter object can be passed around as a parameter, wrapped
// as a singleton or injected using dependency injection.
// Invoke X.y() as follows:
X x = ...
compat.y(x);
The second version looks a bit heavyweight, but it has the advantages that the dynamic (slow, non-type-safe) code is executed just once, and that the version specific code is isolated from the rest of the code. In real life, you would probably put a number of methods into the adapter interface.
This approach requires a bit more thought, to work out how to design the compatibility API so that it cleanly isolates the version dependencies from the rest of the code. You might also to have to revise the adapter API, and create new adapter classes for each new (incompatible) major release.
Finally, if the platform API changes that you need to adapt to entail using classes or methods in the older version that are removed in the newer version, then you will need to compile your various adapter classes (e.g. the CompatV* classes) using different Android SDKs. This will make your build processes rather more complicated.
For other "takes" on this problem, read the following articles on the Android Blog:
Backward compatibility for Android applications
How to have your (Cup-)cake and eat it too.
Here is another example w/o reflection
http://android-developers.blogspot.com/2010/07/how-to-have-your-cupcake-and-eat-it-too.html
See Backward compatibility for Android (using Reflection).
You can check with Build.VERSION.RELEASE, it gives you the current version of your android system (1.5,1.6,2.1,2.2)
There is more on Build.VERSION

Testing if Quicktime and Java are installed?

In Javascript how can I test if the user has the quicktime plugin and java plugins installed?
For Java, you can use navigator.javaEnabled(). Or you can look here: http://www.pinlady.net/PluginDetect/JavaDetect.htm
For QuickTime, you can do:
var QtPlugin = navigator.plugins["Quicktime"];
if (QtPlugin) {//QuickTime is installed}
See here: http://javascript.internet.com/miscellaneous/check-plugins.html
This of course does not work in IE. The only way to check plugins in IE is using VB script, and it is very strange and messy. You can only test for specific plugin versions, for example, "quicktime" won't cut it. You have to specify the version, which is not published for versions older than 5, and I can't find a reference for version 7
The above examples haven't got the answer to the QuickTime part of your question, so here's what I'm writing right now even though the question has been closed and is a little old.
var p = navigator.plugins;
var qtcheck = 0;
for (i=0;i<p.length;i++) {
if (p[i].name.match(/QuickTime/) != null) {
qtcheck++
}
}
if (qtcheck > 0) { // do nothing, QuickTime is intalled }
else {
videos = document.querySelectorAll('object[type="video/quicktime"]')
// use .getElementById instead if there are multiple videos
// replace them with document.createElement('img')
For a more comprehensive and foolproof method i.e. without worry of a plug-in being renamed for whatever reason, you can check within the array of MimeTypes for type="video/quicktime" which is the ultimate answer of whether the object will be supported (or if you're not using the QT video, whatever else you're using it for instead).
This means creating a loop inside the loop through the plugins instead, but is a more firm verification than just a string match:
function checkQT() {
var p = navigator.plugins;
var QT = false; // assume you don't have it
for (i=0;i<p.length;i++) {
for (j=0;j<p[i].length;j++) {
if (p[i][j].type == "video/quicktime") {
QT = true;
return true;
}
else continue;
return false;
}
}
}
I searched around online and found a bunch of great IE fallback scripts here (not sure if this paste service code is going to persist so I gisted it for posterity), from which I took the QuickTime one:
function IEhasQT() {
if (!window.ActiveXObject) return false;
try { if(new ActiveXObject("QuickTime.QuickTime")) return true; }
catch (e) {}
try { if (new ActiveXObject('QuickTimeCheckObject.QuickTimeCheck')) return true; }
catch (e) {}
return false;
}
I tested some others and they just didn't work - catching the exceptions is important.
If you're doing what I'm doing (QuickTime fallback to a gif animation) you might want to take the attributes of the video to provide to the image (or whatever else you're using). The downside to this is that you have to couple it to an onscroll as well as onload (or use Jquery) as the browser is liable to try and find the element before the DOM has loaded no matter how you try and avoid it.
If anyone else reading this is looking for a similar answer, the code to do so is
function noQTfallback() {
var vid1 = document.getElementById("<insert your object id>");
var vid1gif = document.createElement('img');
vid1gif.setAttribute("src","<insert your URL source>");
vid1gif.setAttribute("style",vid1.getAttribute("style"));
document.getElementById("<...>").replaceChild(vid1gif, vid1);
}
function IEhasQT() {
// as above
}
function checkQT() {
// as above
}
function QTbackup(){
if (!checkQT() && !IEhasQT()) {
noQTfallback();
}
}
window.document.body.onload = QTbackup;
window.onscroll = QTbackup;
Oddly, you can have multiple versions of QuickTime installed, my Chrome browser on Windows has 7 copies... Luckily I have a Chromebook which doesn't have QT plug-in either installed or available, so I'm checking and seeing what works to distinguish it, this is the best I've come up with.
I never understood why testing was so important until looking at everyone's awful code on this online, incredible. I know no one cares about IE but basic things like || instead of && are just bad to leave lying around for other developers to reuse.
I've checked this on Windows, Linux and Android (IE and Chrome). The onscroll gives a bit of a jump but without Jquery or some other framework it's unavoidable I guess (and beats "plug-in not supported" !

Categories