Pull to refresh

Самая правильная имлементация сплеш-скрина

Reading time5 min
Views5.3K
Original author: Илья Никсан
Вообще, это перевод моей статьи с medium.com, оригинал лежит тут, подписывайтесь, ставьте лайки, как говорится.

В целом, я видел уже достаточно много статеек на тему как же правильно запилить сплеш на Android, однако все они имеют одну проблему – разрабы забывают о том, что нативные приложения могут вовсе и не иметь единой точки входа – с этим я столкнулся еще в 2009 когда только начинал свой путь разработчика. Представьте себе что вы разрабатываете какой-нибудь клиент для какой-нибудь социалки и кроме android.intent.action.MAIN в вашем манифесте может быть еще с десяток Activity, через которые можно запустить апп – шаринг картинок, текста, нотификации. И по-хорошему везде нужен сплеш!

Тема


Начнем с того, что создадим базовую тему апа с кастомным `android:windowBackground`, в котором будет лежать картинка для нашего сплеша. Базовую тему следует применить ко всем точкам входа как минимум, я же обычно вообще все активити аппа делаю с ней:

<resources>

    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="AppTheme.SplashScreen" parent="AppTheme">
        <item name="android:windowBackground">@drawable/splash</item>
        <item name="android:windowTranslucentStatus">true</item>
        <item name="android:windowTranslucentNavigation">true</item>
    </style>

    ...

</resources>

А еще я добавил android:windowTranslucentStatus и android:windowTranslucentNavigation чтоб сплеш выглядел еще круче! Тег android:windowBackground содержит картинку сплеша которая еще будет фоном и на экране авторизации, я ее стащил с unsplash.com так что упомяну автора kazuend.

Активити


У демо-проекта будет 3 активити: главный экран, авторизация и экран через который можно пошарить что-нибудь. Все они, за исключением авторизации, будут наследоваться от SplashedActivity для проверки статуса авторизации.

private const val ACTIVITY_AUTH = 1000

abstract class SplashedActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        if (!isAuthenticated()) {
            startActivityForResult(Intent(this, AuthActivity::class.java), ACTIVITY_AUTH)
        }
        setTheme(R.style.AppTheme_Base)
        super.onCreate(savedInstanceState)
    }

    private fun isAuthenticated(): Boolean {
        return getUser() != null
    }

    private fun onAuthenticatedCallback(resultCode: Int, data: Intent?) {
        when (resultCode) {
            Activity.RESULT_CANCELED -> finish()
            Activity.RESULT_OK -> recreate()
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        when (requestCode) {
            ACTIVITY_AUTH -> onAuthenticatedCallback(resultCode, data)
        }
        super.onActivityResult(requestCode, resultCode, data)
    }

}

Мы просто запускает авторизацию, ждем результата и пересоздаем себя в случае если авторизация прошла успешно. Вообще мне не очень нравится recreate(), однако многие разрабы делают инициализацию UI в onCreate() и после recreate() она будет вызвана еще раз не потеряв при этом Intent которым Activity была запущена.

Авторизация


Обычно я пользуют AccountManager'ом, но слишком много бойлерплейта нужно чтоб его заюзать, так что хранить все будет в SharedPreferences.



class AuthActivity : AppCompatActivity() {

    private val authCardView by lazy { findViewById<CardView>(R.id.authCardView) }
    private val okButton by lazy { findViewById<Button>(R.id.okButton) }
    private val cancelButton by lazy { findViewById<Button>(R.id.cancelButton) }
    private val loginEditText by lazy { findViewById<EditText>(R.id.loginEditText) }
    private val passwordEditText by lazy { findViewById<EditText>(R.id.passwordEditText) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_auth)
        authCardView.animate()
                .setDuration(500L)
                .setInterpolator(AccelerateDecelerateInterpolator())
                .alpha(1F)
                .setListener(object : Animator.AnimatorListener {
                    override fun onAnimationRepeat(p0: Animator?) {

                    }

                    override fun onAnimationCancel(p0: Animator?) {

                    }

                    override fun onAnimationStart(p0: Animator?) {
                        authCardView.alpha = 0F
                        authCardView.visibility = View.VISIBLE
                    }

                    override fun onAnimationEnd(p0: Animator?) {
                        authCardView.visibility = View.VISIBLE
                    }

                })

        okButton.setOnClickListener {
            performInputChecksAndSaveUser { login, password ->
                saveUser(User(login, password))
                setResult(Activity.RESULT_OK)
                finish()
            }
        }

        cancelButton.setOnClickListener {
            finish()
        }
    }

    private fun performInputChecksAndSaveUser(successCallback: (String, String) -> Unit) {
        if (loginEditText.text.isBlank()) {
            loginEditText.error = getText(R.string.errorEmptyLogin)
        }
        if (passwordEditText.text.isBlank()) {
            passwordEditText.error = getText(R.string.errorEmptyPassword)
        }
        if (loginEditText.text.isNotBlank() && passwordEditText.text.isNotBlank()) {
            successCallback.invoke(loginEditText.text.toString(), passwordEditText.text.toString())
        }
    }
}

Кроме всякой красивой анимации актитвит проверяет ввод юзера, показывает ошибки и собственно сохраняет логин и пароль. Но, если юзер нажмет «Cancel» или кнопку «Назад», активити завершится с резалт-кодом Activity.RESULT_CANCELLED, ActivityManager вернется вверх по стеку и завершит еще и активити которое вызвало авторизацию. Идея этого в том что становится неважно кто вызвал авторизацию – как только любая актитвити, которой нужна авторизация, будет запущена, она проверит, есть ли пользовательские данные в наличии и, если их нет, просто увидит их там же после успешного резульата процесса авторизации.

Шаринг


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.nixan.splashscreenexample">

...

        <activity android:name=".ShareActivity">
            <intent-filter>
                <action android:name="android.intent.action.SEND" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
            </intent-filter>
        </activity>

...

</manifest>

class ShareActivity : SplashedActivity() {

    private val helloText by lazy { findViewById<TextView>(R.id.helloText) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        getUser()?.let {
            helloText.text = "${it.login}\n${intent.getStringExtra(Intent.EXTRA_TEXT)}"
        }
    }
}

Как видите, вообще ничего особенного ни в AndroidManifest.xml, ни в классе активити – только обычная обработка Intent. Заслуга в recreate() – из-за него как раз мы пишем все активити так же как и всегда.

Заключение


И что же сейчас произошло вообще?

ActivityManager обычно требуется некоторое время от момента когда пользователь кликнет на иконку аппа, до того момента как в целевом Activity будет вызван каллбек onCreate(). Для того чтоб замаскировать такой временной лаг, система берет android:theme у активити и рендерит ее без какого-либо контента и только потом контроль передается написанному классу и начинается работа приложения.

Мы объявили что все наши активити будут рендерится с темой, которая выглядит как сплеш-скрин и все они будут унаследованы от SplashedActivity для выполнения проверок авторизации и запуска каких-либо дополнительных экранов инициализации – в нашем случае это только AuthActivity, в котором авторизуется юзер. Еще SplashedActivity будет обрабатывать результат этой авторизации и решать показывать дальше контент аппа пользователю или закрываться.

Таким образом, если пользователь зайдет в Google Play, установит наше приложение и сразу же решит расшарить какой-нибудь контент через этот апп, он попадет на экран авторизации, но после нее он не потеряет контент который он хочет расшарить. Знаю по себе, приложения то я устанавливаю, но вот захожу в них не сразу, так что такой паттерн поведения юзера вполне имеет место быть.

Тут ссылка на проект с примером из этой статьи.
Tags:
Hubs:
+4
Comments4

Articles