воскресенье, 6 сентября 2015 г.

Making fast performance complex animations in android

Recently, I've ran into a problem with animating item's height inside a list for some devices with poor performance. And I've done a library for that. It animates RecyclerView or ListView item's height, but isn't heavy-bounded to that containers - see on github. That animation is also known as expand-collapse animation. Here I describe main problems and solutions founded so far, which is used in library. Library is finished and I'll be very glad if you find some issues or propose pull requests.

So, let's start. To be more clear, I'll write some straightforward code which does what I'm telling about:

ValueAnimator animator = ValueAnimator.ofInt(100, 300);
animator.setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  @Override
  public void onAnimationUpdate(ValueAnimator animation) {
    listViewItem.getLayoutParams().height = (Integer) animation.getAnimatedValue();
    listViewItem.requestLayout();
  }
});
animator.start();
Many libraries, I so before, use this technique. But that is heavy because of requestLayout() - it causes layout and measure steps repeated again each call. From the other side, it has it's rights for living - android developers already introduces nice LayoutTransition which does it's job using exactly same technique: the layout is animated, so layout and measure steps are done again and again until animation finish. That's the only way making CHANGE_APPEARING and CHANGE_DISAPPEARING work when animating for example from layout with items AC to layout with items ABC (C must go down with animating and recalculating everything around to keep layout constraints). So, the following must be considered performance hack. Which may be useful to know when making simple animations.

So, here are some key thoughts:

1. So, all we know, that following animations are fast:
a. Alpha
b. Scale
c. Translate
d. Rotate

2. To change items with just translation and not repositioning other items we can make items heigher than they are so that each next item will hide bottom part of the previos one. They will be like game cards - each one above the other but not fully to see what card is below. This can be achived in many ways:
a. negative bottom margin
b. simply make a wrapper for view which will hold item and clip. FrameLayout does that by default

3. That's not all. Because items are still there. We must care about touch dispatching. Suppose we are clicking on item to expand it. After expand it will be above the previos. We click again for some reason and there is a button on the previos card which steals touch event! User even don't see it, because it is below, but android doesn't use only item's visibility when dispatching touch events. We override dispatchTouchEvent() of item which is animating. So we do simple wrapper for each list item which does the work for us:

    public class VerticalClipLayout extends FrameLayout {
    private float expandCoef = 0; // collapsed by default
    private float clipCoef = 1; // so, full clipping of animated part

    public VerticalClipLayout(Context context) {
        super(context);
        initialize();
    }

    public VerticalClipLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize();
    }

    public VerticalClipLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initialize();
    }

    @TargetApi(21)
    public VerticalClipLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initialize();
    }

    private void initialize() {
        setWillNotDraw(false);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // do not dispatch touch events to view or it's children when clicking the clipped area
        return ev.getY() < getClippedHeight() && super.dispatchTouchEvent(ev);
    }

    // In xml layout_height of VerticalClipLayout is the collapsed height. Inside VerticalClipLayout
    // must be exactly one child view/layout. It's height will be the expanded height.
    // So, setting layout_height="match_parent" for child view is useless.
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if(getChildCount() == 1) {
            View child = getChildAt(0);

            super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, MeasureSpec.getMode(heightMeasureSpec)));
            MarginLayoutParams margins = (MarginLayoutParams)getLayoutParams();
            margins.bottomMargin = MeasureSpec.getSize(heightMeasureSpec) - child.getMeasuredHeight();
        } else {
            throw new IllegalArgumentException("VerticalClipLayout should have exactly 1 child");
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // clipping bottom part to force see video beneath
        canvas.clipRect(0, 0, getWidth(), getClippedHeight()); //Region.Op.REPLACE is no use because some applied clippings (for example from action bar) are not applied sometimes with REPLACE
        super.onDraw(canvas);
    }

    // set's current expand in 0..1 range
    // expandCoef is the coef to expand - y changes when expanding
    public void setExpandCoef(float expandCoef) {
        if(0 > expandCoef || 1 < expandCoef) {
            throw new IllegalArgumentException("expandCoef is out of range [0, 1], expandCoef: " + expandCoef);
        }
        this.expandCoef = expandCoef;

        setTranslationY(expandCoef * ((MarginLayoutParams) getLayoutParams()).bottomMargin);
    }

    // clipHeightCoef is in range 0..1, where 1 means clip all the animated area and show only the allways shown part at the top. 0 means show everything, you'll see fully expaded view
    public void setClipCoef(float clipHeightCoef) {
        if(0 > clipHeightCoef || 1 < clipHeightCoef) {
            throw new IllegalArgumentException("clipHeightCoef is out of range [0, 1], clipHeightCoef: " + clipHeightCoef);
        }
        clipCoef = clipHeightCoef;
        invalidate();
    }

    public float getExpandCoef() {
        return expandCoef;
    }

    // returns y, which view would have, if setExpandCoef(1) was called
    public float getYWhenExpanded() {
        return super.getTop() + ((MarginLayoutParams) getLayoutParams()).bottomMargin;
    }

    public int getExpandedHeight() {
        return getHeight();
    }

    public float getClippedHeight() {
        return getHeight() + clipCoef * ((MarginLayoutParams) getLayoutParams()).bottomMargin;
    }
}
4. I also like that wrapping view, because of encapsulating actual implementation (negative bottom margin or whatever) inside that class to not mess things around.

5. Make an animator class, which just helps us control some ValueAnimator
public class ExpandCollapseAnimator implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener {

    public ExpandCollapseAnimator(float speed);
    public void setOnViewExpandCollapseListener(OnViewExpandCollapseListener listener);

    //call in onResume()
    public void start();

    //call in onPause()
    public void pause();

    // adds view to processing
    public void add(int position, VerticalClipLayout view);

    public void remove(int position);

    // start expand animation for view. Note, that it is neccessary for view to be add()'ed before that call
    public void setExpanding(int position);

    public interface OnViewExpandCollapseListener {
        void onViewStartExpanding(int position, VerticalClipLayout v);
        void onViewExpanded(int position, VerticalClipLayout v);
        void onViewStartCollapsing(int position, VerticalClipLayout v);
        void onViewsChanging();
    }
}

Note: that implementation is compatible with both ListView and RecyclerView. And it has no problems with not animating footer. Thats example:
recyclerView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
            @Override
            public void onChildViewAdded(View parent, final View child) {
                if(recyclerView.getChildViewHolder(child).getItemViewType() != PhoneCardAdapter.VIEW_TYPE_CARD) {
                    return; // just don't add it to our animator
                }
                child.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        child.getViewTreeObserver().removeOnPreDrawListener(this);

                        int position = recyclerView.getChildAdapterPosition(child);
                        animator.add(position, (VerticalClipLayout) child);
                        return false;
                    }
                });
            }

            @Override
            public void onChildViewRemoved(View parent, View child) {
                if(recyclerView.getChildViewHolder(child).getItemViewType() != PhoneCardAdapter.VIEW_TYPE_CARD) {
                    return;
                }
                animator.remove(position);
            }
        });

6. Almost forgot, using OnPreDrawListener() we just assure, that item is positioned, scaled, etc. properly so we can take that things and animate properly.

All this is done as library available with example usage on github

суббота, 2 мая 2015 г.

LibGDX: Kotlin setup

In my previous post I wrote how to configure gradle files to increase build and open IDE speed. Those steps needs to be done if you want to configure kotlin in your libGDX project. Otherwise, you'll get really poor IDE responsiveness and even gradle daemon crashes. Assuming, that you've done all instructions from my previous post, here is the magic steps to use Kotlin. It's easy now:

1. Android Studio (AS) / IDEA -> Settings -> find by "plugin" -> Click "Jetbrains" -> find by "kotlin" -> install kotlin and kotlin for android (optional) plugins and click restart IDE to complete install plugin
2. Open core/src/main directory and create "kotlin" directory there. Recreate directories for your package, like they are in "java" directory. If your app's package is com.mycompany.mygame, then create directory com, open it, create mycompany and so on.
3. Return to IDE. You may try tools -> Kotlin -> Configure kotlin in project... or add bold lines to your core/build.gradle:

apply plugin: 'kotlin'

buildscript {
    ext {
        kotlin_version = '0.11.91.4'
        gdxVersion = '1.5.2'
        box2DLightsVersion = '1.3'
        ashleyVersion = '1.3.1'
        aiVersion = '1.4.0'
    }

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
repositories {
    mavenCentral()
}
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    compile "com.badlogicgames.gdx:gdx:$gdxVersion"
    compile "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
    compile "com.badlogicgames.box2dlights:box2dlights:$box2DLightsVersion"
    compile "com.badlogicgames.ashley:ashley:$ashleyVersion"
    compile "com.badlogicgames.gdx:gdx-ai:$aiVersion"
    compile "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
    compile "com.badlogicgames.gdx:gdx-bullet:$gdxVersion"
}
sourceSets {
    main.java.srcDirs += 'src/main/kotlin'
}

3. Now, you may write kotlin files or convert existing java files to kotlin automatically from IDE. kotlin plugin doesn't need java plugin - it successfully lives in a project with java code and compiles it too. As long, as you keep worlds separate: java files in main/java and kotlin in main/kotlin all is good. If you'll create class com.mycompany.mygame.BestEver in main/java and then create com.mycompany.mygame.BestEver in main/kotlin, you'll get Duplicate class compile error as it must be.
You may find core already converted in my example project https://github.com/Deepscorn/NewLibGDXProjectTemplate. Good luck and happy coding!

Speed up libgdx gradle build in Intellij IDEA

This article describes how to exclude running unnessary tools when building for concrete platform. Here I will make an example for desktop and Android platform, but builds for other platforms can be optimized that way too. The main idea is to move all gradle staff, related to project to it's gradle's build file. So, the way it's designed. Nothing new. While this post may be outdated, see complete and update example project on github . I'm using Android Studio, but IDEA may be used too. And yes, only gradle is here and no Eclipse. I'm a professional programmer and I believe, that Android Studio is far way better than Eclipse if it is set up well.

So, here are the steps to make IDE build fast. Package name everywhere must be your libGDX project package, as usual.
1. Create a new libgdx projects. We need it to get all the latest dependencies versions.
2. Create new Android Application. Targeting 4.0 you'll target > 90 % of devices in the world. Name main class DesktopLauncher, like in project, we created in (1) step.
3. File -> New submodule -> Other (More) -> Java library. Name it "core". "MainGame" is for class name.
4. Now, copy all the source code from MainGame.java from (1) to our new MainGame.java. See, compiler complains? Ok, we are going the right way.
5. Open core/build.gradle and copy all the dependencies from build.gradle (1), you need project(":core") section. It will look something like this:
project(":core") {
    apply plugin: "java"


    dependencies {
        compile "com.badlogicgames.gdx:gdx:$gdxVersion"
        compile "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
        compile "com.badlogicgames.box2dlights:box2dlights:$box2DLightsVersion"
        compile "com.badlogicgames.ashley:ashley:$ashleyVersion"
        compile "com.badlogicgames.gdx:gdx-ai:$aiVersion"
        compile "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
        compile "com.badlogicgames.gdx:gdx-bullet:$gdxVersion"
    }
}
Just copy all the dependencies from here to your new core/build.gradle file in the root. Do not place it under any other tag.
6. As you see, you'll need versions, copy them too. You'll end up with something like that in core/build.gradle:
buildscript {
    ext {
        kotlin_version = '0.11.91.4'
        gdxVersion = '1.5.2'
        box2DLightsVersion = '1.3'
        ashleyVersion = '1.3.1'
        aiVersion = '1.4.0'
    }
}
7. Click "gradle sync" button (green in circle with an arrow). If MainGame.java still not compiling, add the repo dependencies to core/build.gradle:
repositories {
    mavenCentral()
}
8. Now it must compile well. You'll get something like that:
apply plugin: 'java'

buildscript {
    ext {
        gdxVersion = '1.5.2'
        box2DLightsVersion = '1.3'
        ashleyVersion = '1.3.1'
        aiVersion = '1.4.0'
    }
}
repositories {
    mavenCentral()
}
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    compile "com.badlogicgames.gdx:gdx:$gdxVersion"
    compile "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
    compile "com.badlogicgames.box2dlights:box2dlights:$box2DLightsVersion"
    compile "com.badlogicgames.ashley:ashley:$ashleyVersion"
    compile "com.badlogicgames.gdx:gdx-ai:$aiVersion"
    compile "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
    compile "com.badlogicgames.gdx:gdx-bullet:$gdxVersion"
}
9. Repeat steps 4-8 to android (app) project: copy code in AndroidLauncher.java from (1). And don't copy natives. You'll get something like that in android/build.gradle:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "com.gamelift.koten"
        minSdkVersion 14
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

buildscript {
    ext {
        gdxVersion = '1.5.2'
        box2DLightsVersion = '1.3'
        ashleyVersion = '1.3.1'
        aiVersion = '1.4.0'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':core')
    compile "com.badlogicgames.gdx:gdx-backend-android:$gdxVersion"
    compile "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
    compile "com.badlogicgames.box2dlights:box2dlights:$box2DLightsVersion"
    compile "com.badlogicgames.ashley:ashley:$ashleyVersion"
    compile "com.badlogicgames.gdx:gdx-ai:$aiVersion"
    compile "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
    compile "com.badlogicgames.gdx:gdx-bullet:$gdxVersion"
}

10. Copy application theme to res/values/styles.xml. You'll get something like this:
<resources>

    <style name="AppTheme" parent="android:Theme">
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:colorBackgroundCacheHint">@null</item>
        <item name="android:windowAnimationStyle">@android:style/Animation</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowFullscreen">true</item>
    </style>

</resources>

11. Update AndroidManifest.xml according manifest from (1). It will look like:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.gamelift.koten.android" >

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".AndroidLauncher"
            android:label="@string/app_name"
            android:screenOrientation="landscape"
            android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
12. Now android should compile.
13. If you run your app now (just by clicking "run" (green button on the top)) it'll crash. It's because we've not copied libraries, implemented in "c" language. You may just copy all files from android/libs (1) to android/src/main/jniLibs like that is done in Unity when you export android project :) Running now you will see badlogic's standart smile on red screen.
13. For desktop project we create File -> New submodule -> Other (More) -> Java library project.
14. "desktop" is for name. "DesktopLauncher" is for class name to create automatically.
15. Repeat steps 4-8 to desktop. You must get something like that in desktop/build.gradle:

apply plugin: 'java'

buildscript {
    ext {
        gdxVersion = '1.5.2'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project(":core")
    compile "com.badlogicgames.gdx:gdx-backend-lwjgl:$gdxVersion"
    compile "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop"
    compile "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-desktop"
    compile "com.badlogicgames.gdx:gdx-tools:$gdxVersion"
    compile "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-desktop"
    compile "com.badlogicgames.gdx:gdx-bullet-platform:$gdxVersion:natives-desktop"
}
16. To run desktop configuration go to run... -> Edit configurations... -> Click "+" -> Application (Java) and provide the following:
<packages>.DesktopLauncher for main class
../android/src/main/assets for working directory
and name it "desktop" for example
17. (optional) If you want to rename "app" -> "android" it's not straitforward. The most safe way is to shutdown Android Studio. Delete .idea and all *.iml files. Now, rename directory "app" -> "android". Open settings.gradle in text editor and change ":app" to ":android". You'll get something like:

include ':android', ':core', ':desktop'

Now start Android Studio. Import project (gradle). It will take more time, than usual.

Working project can be found on my github. So, what we've archived? As I said earlier we don't waste time waiting for some plugins or dependencies to be downloaded, indexed, etc. We just run for concrete platform and concrete plugins are loaded and used to compile and run. On my Core i3 pc I've got about 10 times increased build time. And now I don't need to make a cup of coffee while AS is opening my project.

Let's think, what drawbacks were in old build.gradle files. At the time of writing this post libgdx has one main build.gradle at the root of the project and many build.gradle specified per each platform (one in android, one in desktop...).

1. So, main build.gradle contains all the plugins:
buildscript {
    dependencies {
        classpath 'de.richsource.gradle.plugins:gwt-gradle-plugin:0.6'
        classpath 'com.android.tools.build:gradle:1.0.0'
        classpath 'org.robovm:robovm-gradle-plugin:1.0.0-beta-01'
    }
}
I don't need all those plugins when I just want to build desktop for example.

2. And allprojects section, which apply some plugins too:

allprojects {
    apply plugin: "eclipse"
    apply plugin: "idea"
}

"eclipse" is not needed. "idea"'s modern plugin implementation is used by default.
See complete (and updated) example on github

Will Kotlin be the future for libGdx to target Web platform?

Currently, libgdx uses GWT plugin to target web platform. And GWT doesn't support even Java 1.6! See official docs. And it allways needs source files! (see GWT/html section in libgdx docs) It's not allways affordable.

What about Kotlin? It's robust, brand new language which is fully compatible with JVM, starting from Java 1.6+!

Let's see what's about integration in modern IDE's. Kotlin plugin for Android Studio / IDEA successfully compiles both .kt and .java files to .class! (see here and here for android)

And what about starting to code best libGDX projects in Kotlin and deploy them to web now? Now we have libgdx backends only for GWT...

What we may try now?

1. Kotlin can be converted to javascript thanks to JetBrains. Here is an example of web project, where javascript is generated from kotlin code. So, we may do that generation for now (hoping things become better in the future) and add generated javascript to GWT's web project to use with libgdx gwt backends as usual. In theory, it could work.

2. Or we may try investigating about current possibilities of Kotlin in conversion to javascript, running HTML5. Get gwt backend sources as an example and write our own backend for Kotlin's javascript and contribute to libGDX?

What will you say? Interesting, isn't it? I'll be glad to see your thoughts! Post comments, please


понедельник, 6 октября 2014 г.

I'm seeking for a job

Hello, everyone! I'm seeking for a job as a libGDX / Android / Java game developer. I'm living in Saint-Petersburg, Russia. We'll be glad to cooperate. Mailto: vnms11@gmail.com.

Here is my experience.
I'm most interested in: libGDX, cross-platform, AI, fuzzy logic, 2D game development from scratch, OpenGL, java, interesting algorithms.
Can sometimes do jobs, associated with: pure Android SDK, Python, Android NDK, jUnit, client-server, SQL.
See my old-old game on google play market.
Or my site with portfolio (sorry for advertisements there).
If you seek for commercial developments, see navigational simulator. Here I worked as a part of team for 2 years. And another 3 years I've been developing different integration software for ENOVIA V6, SmarTeam, CATIA, SolidWorks and I've been working with clients (see Dassault site).

Serpent's Madness 2012


Serpent's Madness is back! It was first developed for windows platform. Now you can play on android. Improved serpent movement, smarter AI and respawn feature included. Here serpent AI has "weights" and done like in fuzzy logic. Code is open, listed in my post on habrahabr. Graphics is done by professional designer.

Platform: android
View on market
Download
Developed with Android SDK

Rock Carrier

Rock Carrier is my try to write a free fan game to Space Rangers 2, developed by Elemental Games. It's a complete new game with graphics and sound from original game. As allways, I developed it from scratch. It is an arcade space real-time shooter. Game is loading for 2-5 minutes. When done, press space bar to play. You control dominator space ships with 2 type of attacks (on left and right mouse buttons). Pirate ships try to destroy you. There is a giant star, asteroid rings and space stations on the level. If you'll get killed, you will respawn at left top corner of level. Your health is displayed in left bottom corner of the screen. Your ship follows the mouse cursor.

Developed with own engine IZeng, C++, corona library
Download
Platform: Windows