Wednesday, 18 July 2012

Marmalade splash screen issue resolved (for Android)


The glorious hour's gone forth, I finally managed to display the splash screen in a Marmalade-based android application in a proper way.

As you may know there is a problem with splash screens in Marmalade SDK. That problem is not new - it has been around a year since it first emerged link to the marmalade forum. Basically this problem manifests itself (at least on Android) with a long blank screen period after program launch. After this a splashscreen is shown (either yours or standard) followed by the short blank screen period, and only after that your main() function is triggered. This might be due to initialization of glContext, however, it takes longer for the apps with more resources.

Some people suggest to show splashscreen manually from main(), but I cannot agree with this proposal because it is always too late to launch fireworks from main() see this. I've decided to approach this issue in a different way. For Android it is possible to override standard Marmalade's LoaderActivity with your own custom activity. I think you have already guessed what it means, and yes this approach works.

Now for the details and sample code. My custom activity is based on the "examples\AndroidJNI\s3eAndroidLVL" source provided with Marmalade.
package com.android.mainactivity;

import com.ideaworks3d.marmalade.LoaderActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.content.Context;
import android.widget.ImageView;
import android.view.ViewGroup.LayoutParams;
import java.util.Timer;
import java.util.TimerTask;
import android.view.ViewGroup;

public class MainActivity extends LoaderActivity {
 
    private static MainActivity m_Activity;
    private ImageView m_ImgView;
 
    private final Runnable m_UiThreadStartLogo = new Runnable(){
       public void run(){ 
           m_ImgView = new ImageView((Context)LoaderActivity.m_Activity);
           m_ImgView.setBackgroundResource(0x7f020001);
           m_ImgView.setVisibility(View.VISIBLE);
           LoaderActivity.m_Activity.addContentView(m_ImgView, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
       }
    };
 
    private final Runnable m_UiThreadStopLogo = new Runnable(){
       public void run(){ 
           if (m_ImgView != null){
               //m_ImgView.setVisibility(View.INVISIBLE);
               ViewGroup vg = (ViewGroup)(m_ImgView.getParent());
               vg.removeView(m_ImgView);
           }
       }
    };

    public class CustomTimerTask extends TimerTask {
       public void run() {
           LoaderActivity.m_Activity.runOnUiThread(m_UiThreadStopLogo);
       }
    }

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        m_Activity = this;
        LoaderActivity.m_Activity.runOnUiThread(m_UiThreadStartLogo);
  
        Timer timer = new Timer();
        TimerTask updateProfile = new CustomTimerTask();
        timer.schedule(updateProfile, 4000);
    }

    protected void onDestroy() {
        super.onDestroy(); 
    }
}

After building the jar file do not forget to mention it in mkb file, like this:
deployments
{
 android-custom-activity='com.android.mainactivity.MainActivity'
 android-external-jars='NativeExtensions/SplashScreenFix/java/mainactivity.jar'
...
}

In the "onCreate" method of the custom MainActivity I launch the ImageView. This ImageView is set to show the splash image. In the above case for simplicity I have put a numerical id for an image that is included in my Marmalade project through Gallery Icon setting (it doesn't have to be 170x170 if you would check the "Allow non-standart icon sizes checkbox). When the project is built for android target this image will be located in: "...\android\release\intermediate_files\res\drawable"
and you can find its numerical id in:
"...\android\release \intermediate_files\src\com\proj\myproj\R.java".

Now the splash image will be shown immediately after launch. The only trouble it woudn't go away automatically, effectively covering all your app) In the above code I hide it on timer, that is set to 4 seconds delay. The better approach would be to use jni interface and call stopLogo as the first instruction in your main(). Anyhow, I would still prefer to have timer as a last resort, to be sure that the logo screen will hide eventually.

I have used Maramalde 6.0.5 SDK.

Update:

Q:Can you give the c++ code + java modification to call stopLogo as JNI call instead of timer?

A: In your MainActivity declare "static MainActivity m_Activity" field and initialize it in OnCreate() to "this", also declare "public boolean StopMyLogo()" method, that would actually hide th logo view. To call this method from c++ take as a reference \Marmalade\6.0\examples\AndroidJNI\s3eAndroidLVL\source\s3eAndroidLVL.cpp. You should write something like this (error checking omitted):
void StopMyLogo()
{
 if (!s3eAndroidJNIAvailable()) return;
 JavaVM* jvm = (JavaVM*)s3eAndroidJNIGetVM();
 JNIEnv* env = NULL;
 jvm->GetEnv((void**)&env, JNI_VERSION_1_6);
 jclass Activity = env->FindClass("com/android/mainactivity/MainActivity");
 jfieldID fid = env->GetStaticFieldID(Activity, "m_Activity", "Lcom/android/mainactivity/MainActivity;");
 jobject m_Activity = env->GetStaticObjectField(MainActivity, fid);
 jmethodID pMethod = env->GetMethodID(MainActivity, "StopMyLogo", "()Z");
 env->CallVoidMethod(m_Activity, pMethod); //Or CallBooleanMethod()
}


Update 2 (as of September 2013) :

If you will follow Google's optimization advices for tablets and change targetSdkVersion to "14", your activity will be restarted on screen rotation event, triggering onCreate() method again! You don't want the splash screen to reappear when the user rotates a phone. Make sure you run m_UiThreadStartLogo just once. For example, you can add to activity's settings in the manifest android:configChanges="orientation|screenSize". This way activity would not be restarted when screen is about to rotate.

20 comments :

  1. Incredible example!!! Thank you VERY MUCH!
    But I have some issue. I can show splash screen only in the upper-left corner of screen. I want set square image to center (so it can be shown good on each resolutions).
    I put it on the relative layout, and set to relative layout gravity - center. But it doesn't works.
    May be you have any idea?

    ReplyDelete
    Replies
    1. Oh, don't worry - I've fixed it
      Center gravity to relative layout - is the right way
      but I also set Layout Params to this layout - FILL_PARENT

      Delete
  2. How do I compile this? Can you please post an archive we can download?

    ReplyDelete
    Replies
    1. Create directory structure inside your project SplashFix\java\com\android\mainactivity. Place MainActivity.java there. Copy s3eAndroidLVL_java.mkb from \Marmalade\6.0\examples\AndroidJNI\s3eAndroidLVL to SplashFix\ root. Chage "files {...}" section inside mkb to:
      files
      {
      (java/com/android/mainactivity)
      MainActivity.java
      }
      Save it and double click on it. MAinActivity should now be compiled and in \SplashScreenFix\java you'll find mainactivity.jar. Now you should put a reference to this jar in your project's mkb file, as shown in the post.

      Delete
    2. Oh, and also change the options section in s3eAndroidLVL_java.mkb like this:

      options
      {
      output-name='java/mainactivity.jar'
      }

      Delete
  3. By the way your blog is formatting my name wrong. You should be able to fix it using these instructions http://support.google.com/blogger/bin/static.py?hl=en&selected=there_are_funny_characters_in_my_blog&page=troubleshooter.cs&ctx=my_blog_is_displaying_funny_there_are_funny_characters_in_my_blog_41397&problem=my_blog_is_displaying_funny

    ReplyDelete
  4. I could kiss you! ;)
    (Been through Marmalade splash screen hell and back)
    I've found android:screenOrientation="landscape" useful in my manifest too to prevent flipping.

    ReplyDelete
  5. Wow android:screenOrientation="landscape" just solved half my problem. I updated marmalade with that info.

    ReplyDelete
  6. Thank you so much. i have no problems with splash screen but thanks to your post, I learned how to create custom activity <3

    ReplyDelete
  7. Can you give the c++ code + java modification to call stopLogo as JNI call instead of timer?

    Thanks

    ReplyDelete
    Replies
    1. I had problems with your solution with this stack trace :
      android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
      So what I did was to launch the timer as in your original code, but instead of closing the background image in m_UiThreadStopLogo, simply sleep loop and wait for a boolean to be modified, which is modified via JNI. If I was not launching the timer, it seems the activity was simply blocked. It's not the perfect solution IMO but it seems to work... If you have better idea don't hesitate to share.

      Delete
    2. Hi,
      Sure you would get this exception when you try to modify android UI from a non-UI thread (that is marmalade's main thread).

      Use:
      public boolean StopMyLogo()
      {
      LoaderActivity.m_Activity.runOnUiThread(StopLogoFromUiThread);
      }

      Where:

      private final Runnable StopLogoFromUiThread = new Runnable()
      {
      public void run()
      {
      //Hide your logo from here!
      }
      };

      Delete
  8. Any idea how to center the splash screen without any resizing?

    ReplyDelete
    Replies
    1. You may want to use ViewGroup:

      m_ImgView = new ImageView((Context)LoaderActivity.m_Activity); m_ImgView.setScaleType(ImageView.ScaleType.CENTER); m_ImgView.setImageResource(0x7f020001);

      Delete
    2. Thanks! And if I would like to change the background color of the splash screen while centering it, should I play with layouts?

      Delete
  9. Here is my code in run() allowing me to center and resize the image, without any stretching. My problem is that I also want the remaining space (which is currently black) to be white. What would be the easiest way to do the same work?

    m_ImgView = new ImageView((Context)LoaderActivity.m_Activity);
    m_ImgView.setBackgroundResource(0x7f020001);
    m_ImgView.setVisibility(View.VISIBLE);

    int windowHeight = getWindowManager().getDefaultDisplay().getHeight();
    int windowWidth = getWindowManager().getDefaultDisplay().getWidth();
    int newSize = (windowHeight <= windowWidth) ? windowHeight : windowWidth;
    m_ImgView.setMinimumHeight(newSize);
    m_ImgView.setMinimumWidth(newSize);

    LinearLayout llayout = new LinearLayout(LoaderActivity.m_Activity);
    llayout.addView(m_ImgView);
    llayout.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL);
    LoaderActivity.m_Activity.addContentView(llayout, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));

    Thanks!

    ReplyDelete
    Replies
    1. m_ImgView.setBackgroundColor(Color.rgb(255, 255, 255));

      Delete
  10. Hey mate, thanks for the article. This is exactly the fix we need. However, we don't have an R.java file to find the resource id. We've searched our entire project folder, and the contents of src/proj/ourproject/ were just empty. Thanks for any insight you can provide.

    ReplyDelete
  11. This is great!.. thanks for this post!..

    I have a question though. I am using marmalade quick, and I would like to call the c++ code that uses JNI to hide the splash from my own lua code.

    Is there any way to do so without extending quick itself?

    Thanks a lot,
    Lucas

    ReplyDelete