创建一个真正的启动画面

11

我怎样才能在Android中制作一个真正的启动画面?我不想用计时器或延迟,只需要一个显示直到应用程序加载完成的启动画面。


可以查看:http://blog.blundell-apps.com/tut-splashscreen-with-progress-bar/ - Blundell
7个回答

8
下面提供了一个带有代码的解决方案,过去六周在多个测试设备上似乎运行良好。但是,在进行完整的启动画面之前,应该考虑一些准备工作。
首先,如果您可以通过立即显示应用程序的主视图来避免需要启动画面,则这是最好的选择。您通常可以通过立即显示主显示器的图形,然后创建一个工作线程来执行任何耗时的初始化任务(例如加载应用程序始终使用的表格)来实现此目的。
然而,如果您的主视图本身设置和显示需要很长时间,您可能希望在此期间看到其他内容。如果您的主活动具有简单(例如默认)、浅色或黑色、非透明背景,那么这将提供即时确认,用户启动应用程序时至少会看到“某些东西”。我个人发现以下背景主题可用作基本的“启动画面”显示(添加到清单文件中主活动的活动标记中)。
android:theme="@style/Theme.Light.NoTitleBar"
android:theme="@style/Theme.Black.NoTitleBar"

我想指出的是,如果您的活动背景需要任何一种位图、动画或其他可绘制对象超出默认主题(或简单的浅色或黑色主题),那么我的经验是,在您的主视图显示之前,活动背景将不会显示,因此仅将活动背景更改为自己的启动显示不会比您的主屏幕提供更快的响应(根据我的经验)。
虽然以上简单的主题可以作为原始的“闪屏”,但也许您认为简单的浅色或黑色活动背景过于平淡,不能提示用户您的应用程序已启动,您希望在等待时显示应用程序的名称或标志。或者,您的活动背景必须是透明的,因为您希望能够在其他应用程序上叠加自己应用程序的视图(这样的透明背景在启动期间当然是不可见的,因此不会提示用户您的应用程序已启动)。
如果在考虑了上述所有替代方案之后,您仍然认为您需要一个闪屏屏幕,这里有一种方法,我个人发现非常有效。
对于这种方法,您需要定义一个扩展LinearLayout的新类。您需要自己的类的原因是,您需要收到正面确认,即您的闪屏屏幕已经显示,这样您就可以立即开始显示主视图,而不需要一些只能猜测闪屏屏幕需要多长时间才能出现的计时器。我想指出的是,如果您在显示闪屏视图后太快地启动主视图的显示,闪屏视图将永远不会被看到;使用这种方法可以避免这种可能性。
以下是这样一个类的示例:
public class SplashView extends LinearLayout {

    public interface SplashEvents {
        //This event is signaled after the splash and all of its child views, 
        // if any, have been drawn.
        // As currently implemented, it will trigger BEFORE any scrollbars are drawn.
        // We are assuming that there will BE no scrollbars on a SplashView.
        public void onSplashDrawComplete();
    }

    private SplashEvents splashEventHandler = null;

    public void setSplashEventHandler(SplashEvents splashEventHandler) {
        this.splashEventHandler = splashEventHandler;
    }

    private void fireSplashDrawCompleteEvent() {
        if(this.splashEventHandler != null) {
            this.splashEventHandler.onSplashDrawComplete();
        }
    }

    public SplashView(Context context) {
        super(context);

        //This is set by default for a LinearLayout, but just making sure!
        this.setWillNotDraw(true);

        //If the cache is not enabled, then I think that helps to ensure that
        // dispatchDraw override WILL
        // get called.  Because if caching were enabled, then the 
        //drawing might not occur.
        this.setDrawingCacheEnabled(false);

        setGravity(Gravity.CENTER);

        //This splices in your XML definition (see below) to the SplashView layout
        LayoutInflater.from(context).inflate(R.layout.splashscreen, this, true);
    }


    @Override
    protected void dispatchDraw(Canvas canvas) {
        //Draw any child views
        super.dispatchDraw(canvas);

        //Signal client objects (in this case, your main activity) that 
            // we have finished initializing and drawing the view.
        fireSplashDrawCompleteEvent();
    }
}

由于我们从视图内部加载XML,因此我们需要在XML中使用<merge>标签将其定义为SplashView类的子元素插入XML定义的元素。这是一个示例(应放置在您的应用程序的res/layout文件夹中),您可以根据自己的需要进行调整:

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:gravity="center_horizontal"
    >
    <TextView  android:id="@+id/tvHeading"
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent"
        android:gravity="center"
        android:textSize="30dp"
        android:textStyle="bold"
        android:text="Loading..."
        android:layout_weight="1.0"
        android:textColor="#00ff00"
        android:background="#AA000000"
        />
</merge>

请注意,TextView 的定义带有半透明黑色背景,这样它将导致启动器显示变暗,文本“Loading...”以绿色叠加在其上。
现在只需要将类似以下内容编辑到您的主活动的 onCreate() 方法中(并在其上方):
private Handler uiThreadHandler = new Handler();

@Override
public void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    //Create an instance of the splash view, and perform a setContentView()
    SplashView splashView = new SplashView(this);

    //Doing this allows you to access the "this" pointer of the main 
        // activity inside the Runnable below.
    final main mainThis = this;

    // Set an event handler on the SplashView object, so that as soon 
    // as it completes drawing we are
    // informed.  In response to that cue, we will *then* put up the main view, 
    // replacing the content view of the main activity with that main view.
    splashView.setSplashEventHandler(new SplashView.SplashEvents() {
        @Override
        public void onSplashDrawComplete() {
            //Post the runnable that will put up the main view
            uiThreadHandler.post(new Runnable() {
                @Override
                    public void run() {
                        //This method is where you will have moved 
                        // the entire initialization of your app's 
                        // main display, which normally would have been 
                        // in your onCreate() method prior to adding the 
                        // splash display.
                        launchMainView(mainThis, savedInstanceState);
                    }
            });
        }
    });

    //This will cause your splash view to draw.  When it finishes, it will trigger the code above.
    this.setContentView(splashView);

    //At this point, do *not* move directly on to performing a setContentView() on your main view.
    // If you do, you will never see the splash view at all.
    // You simply wait for the splash view to signal you that it has completed drawing itself, and
    // *then* call launchMainView(), which will itself call setContentView() again, passing it
    // your main view.
}

//Here is a stripped-down version of launchMainView().  You will typically have some additional
// initializations here - whatever might have been present originally in your onCreate() method.
public void launchMainView(main mainThis, Bundle savedInstanceState) {
    myRootView = new MyRootView(mainThis);

    setContentView(myRootView);
}

上述方法对我来说非常有效。 我仅针对API级别8进行了使用,并在各种设备上进行了测试,包括运行Android 2.2.1、2.3.3和4.0.1(ICS)的手机和平板电脑。
注意:上述方法的潜在风险是,在某些情况下,启动画面可能不会发出已完成的信号,因此启动画面将“卡住”主显示器,而没有主视图来替换它。这种情况从未发生过,但我想征求您关于SplashView中调用dispatchDraw() 方法是否可能未被调用的评论。我对触发dispatchDraw() 的代码进行了目测检查,并且在SplashView构造函数中进行了初始化,因此看起来它将始终得到调用。
如果有人有更好的方法来覆盖同一目的,请告诉我。我很惊讶我找不到任何专门定制为在视图显示完成时触发的覆盖,因此,如果存在我错过的覆盖,请在下面发布评论。欢迎确认此方法有效的评论!

1
对我来说非常好用,我喜欢你的解决方案,希望将来不会出现任何错误。谢谢! - idish
同样地,我在多个应用程序中使用这种方法时没有遇到任何问题,自己测试了从Froyo到Jelly Bean的设备,并且公开分发了多个月而没有收到任何投诉。 - Carl
为什么不使用“onDraw”,并在那里调用您的函数一次(使用标志来保护它不被多次调用)? - android developer
启动画面的子视图会在启动画面自身的onDraw()方法调用后被绘制,以便出现在其前面,因此从onDraw()触发onSplashDrawComplete()将会在启动主视图之前开始主视图的显示,而且启动画面在主视图出现之前也不会变得可见。dispatchDraw()方法绘制了所有启动画面的子视图,因此通过重写并在触发事件之前调用super.dispatchDraw(),我们可以确保在启动主视图之前绘制子视图。 - Carl
1
+1 对于“让用户立即访问您的应用程序,这是您最好的选择。”完全同意(作为一名工程师可能?) - Attacktive

3

类似于

public void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    setContentView(R.layout.splash);

    handler = new Handler();

    new AsyncTask<Void, Void, Void>() {

    @Override
    protected Void doInBackground(Void... params) {
            //Do some heavy stuff
            return null;
        } 

        @Override
        public void onPostExecute(Void result){
            handler.post(new Runnable(){
                 @Override
                 public void run(){
                     setContentView(R.layout.main);
                 }
            });
        }
   }.execute();
}

1
如果重要的东西恰好是setContentView(R.layout.main)呢? - slipbull
1
你不应该在一个Activity中两次调用setContentView。我建议你可以调用startActivity(在启动画面之后要加载的Activity)。 - jsb
这对我不起作用。屏幕停留在第一个启动画面,而没有设置为第二个加载画面。我使用了几乎相同的代码,只是我没有在run方法上使用@Override标记,因为Eclipse将其作为quickFix删除,提示:new Runnable(){}类型的run()方法必须覆盖超类方法。 - kaid
实际上是什么启动了AsyncTask? - kaid
我在代码中添加了.execute(),复制粘贴时一定出了问题 :) - nhaarman

2

这并不难,你只需要创建一个视图作为启动屏幕(它具有简单的布局且不需要大量测量),使用setContentView将其设置为活动内容;

然后再次在活动上调用setContentView,并使用需要花费时间来构建的复杂布局。你甚至可以使用Asynctask在第二次使用复杂布局之前加载数据。这取决于你是受数据加载还是视图构建的限制。


如果调用setContentView()来显示闪屏视图,然后立即为主视图再次调用它,则在第二个视图显示之前,闪屏视图将没有机会显示。现在,如果您有一个工作线程在闪屏显示后执行繁重的任务,并且线程发布到UI处理程序以执行最终的setContentView(),那么应该可以正常工作,只要异步操作需要一段时间。但是,如果您的延迟来自于主视图本身(根据slipbull的评论),则需要等待闪屏绘制完成后再触发第二个setContentView()。 - Carl

1
public class MyLocationListener extends Activity {

    public Handler myHandler = new Handler(){
             public void handlerMessage(Message msg){
                  set you contentView here after splash screen
             }
          } 

    public MyLocationListener(Context context){
        this.context = context;
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                 setContentView(R.layout.splashscreen);
                // don't set Content View here instead start a thread here to do the task yiou want to do. 
                // Now post message from new thread to UI Thread using handlers.

         }

}

1

做启动画面的最佳方式是:

  1. 如果用户按下返回键,您需要快速返回到主屏幕。
  2. 不要使用动画效果。

我找到了这个好的解决方案:

import android.app.Activity; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.view.WindowManager; import br.eti.fml.android.sigame.R;

import java.util.concurrent.atomic.AtomicBoolean;

public class LauncherActivity extends Activity {
    private AsyncTask goingToNextScreen;
    private AtomicBoolean alreadyShown = new AtomicBoolean(false);

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.launcher);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        //noinspection unchecked
        goingToNextScreen = new AsyncTask() {

            @Override
            protected Object doInBackground(Object... objects) {
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    // ignores
                }

                return null;
            }

            @Override
            protected void onPostExecute(Object o) {
                goNext();
            }
        }.execute();
    }

    @Override
    public void onBackPressed() {
        if (goingToNextScreen != null) {
            goingToNextScreen.cancel(true);

            goNext();
        }
    }

    private void goNext() {
        if (alreadyShown.compareAndSet(false, true)) {
            startActivity(new Intent(LauncherActivity.this, HomeActivity.class));
            overridePendingTransition(0, 0);
            finish();
            overridePendingTransition(0, 0);
        }
    } }

1

有时候使用启动屏幕(Splash)时,应用程序需要几毫秒或几秒钟来加载 Activity 的内容。

如果你只想要一个“背景图像”作为通常的启动屏幕,我认为最好的方法是使用主题(Themes)。

例如,可以使用 SherlockActionBar:

<style name="SplashTheme" parent="Theme.Sherlock.NoActionBar">
    ...
    <item name="android:windowBackground">@drawable/splash</item>
    ...
</style>

其中闪屏可以是一个.9文件来填充屏幕。

而在清单中的Activity必须是类似于以下内容:

    <activity
        android:name=".SplashActivity"
        ...
        android:theme="@style/SplashTheme"
        ...>
        ...
     </activity>

如果您的代码中没有setContent(View)行,则不需要它。主题将比内容加载更快。

这使您可以在应用程序加载开始时拥有启动画面。没有黑色窗口、操作栏或类似的东西。


-3

//Splash代码的代码

public class SplashScreen extends Activity {

    static int SPLASH_TIMEOUT = 5000;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.splash_layout);

    new Handler().postDelayed(new Runnable() {

        @Override
        public void run() {
            startActivity(new Intent(SplashScreen.this, MainActivity.class));
            finish();
        }
    }, SPLASH_TIMEOUT);
    }
}

这里SPLASH_TIMEOUT将定义您自己的活动应该在多长时间后显示,因此根据您的需要更改此值。

// MainActivity.class 的代码

public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }

}

OP 表示他不想使用定时器或延迟。请仔细阅读问题。 - stealthjong

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接