From 53ffc2d2fcaaa00a5deb37242368930e4788ebc2 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 12 Sep 2019 13:57:07 +0300 Subject: [PATCH] Added navigation-new module --- navigation-new/.gitignore | 1 + navigation-new/README.md | 123 +++++++ navigation-new/build.gradle | 40 +++ navigation-new/src/main/AndroidManifest.xml | 3 + .../navigation/FragmentNavigation.kt | 288 ++++++++++++++++ .../navigation/SimpleActionBarDrawerToggle.kt | 208 ++++++++++++ .../components/navigation/TouchinApp.java | 147 +++++++++ .../navigation/activities/BaseActivity.kt | 92 ++++++ .../activities/NavigationActivity.kt | 25 ++ .../activities/OnBackPressedListener.java | 7 + .../navigation/fragments/BaseFragment.kt | 0 .../fragments/ViewControllerFragment.kt | 259 +++++++++++++++ .../KeyboardBehaviorDetector.kt | 67 ++++ .../KeyboardResizeableViewController.kt | 82 +++++ .../navigation/viewcontrollers/EmptyState.kt | 19 ++ .../LifecycleLoggingObserver.kt | 16 + .../viewcontrollers/ViewController.kt | 309 ++++++++++++++++++ .../ViewControllerNavigation.kt | 150 +++++++++ navigation/build.gradle | 6 - 19 files changed, 1836 insertions(+), 6 deletions(-) create mode 100644 navigation-new/.gitignore create mode 100644 navigation-new/README.md create mode 100644 navigation-new/build.gradle create mode 100644 navigation-new/src/main/AndroidManifest.xml create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.kt create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/activities/NavigationActivity.kt create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java rename {navigation => navigation-new}/src/main/java/ru/touchin/roboswag/components/navigation/fragments/BaseFragment.kt (100%) create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.kt create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardResizeableViewController.kt create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/LifecycleLoggingObserver.kt create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.kt create mode 100644 navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt diff --git a/navigation-new/.gitignore b/navigation-new/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/navigation-new/.gitignore @@ -0,0 +1 @@ +/build diff --git a/navigation-new/README.md b/navigation-new/README.md new file mode 100644 index 0000000..d470eef --- /dev/null +++ b/navigation-new/README.md @@ -0,0 +1,123 @@ +navigation +==== + +Модуль содержит классы для организации навигации в приложении. + +### Основные интерфейсы и классы + +#### Пакет `activities` + +`BaseActivity` - абстрактный класс, в котором выполняется логгирование с помощью модуля [logging](https://github.com/TouchInstinct/RoboSwag/tree/master/logging) при выполнении некоторых методов. Класс позволяет добавлять новые `OnBackPressedListener` и удалять их с помощью методов *addOnBackPressedListener* и *removeOnBackPressedListener* (*removeAllOnBackPressedListeners*) соответственно. + +Интерфейс `OnBackPressedListener` - интерфейс с одним методом *onBackPressed*. Используется в `BaseActivity`. + +#### Пакет `fragments` + +Класс `ViewControllerFragment` наследуется от `Fragment`. Через статический метод *args* получается `Bundle` с классом `ViewController`(а) и состоянием, которое наследуется от `Parcelable`. В методе *onCreate* инициализируются поля *state* и *viewControllerClass* используя данные из `Bundle`. В методе *onCreateView* создается `ViewController`. + +#### Пакет `viewcontrollers` + +`ViewController` - обертка над Fragment. Один ViewController - один экран. К моменту инициализации вашего класса уже будут доступны следующие поля из `ViewController`: *state*, *activity*, *fragment*, *view*. Это означает, что можно выполнять всю настройку экрана в `init { }`. + +У класса есть два параметра `TActivity: FragmentActivity` и `TState: Parcelable`, которые нужно указывать при инициализации класса `ViewController`. В конструкторе данный класс принимает `CreationContext` и идентификатор layout-ресурса. + +`ViewControllerNavigation` отвечает за навигацию по `ViewController`(ам). В конструкторе принимает `Context`, `FragmentManager` и идентификатор ресурса, который является контейнером для других фрагментов. Имеет параметр `TActivity : FragmentActivity`. + +`EmptyState` - пустое состояние. Использутся, когда при переходе к новому `ViewController` не нужно передавать никаких инициализирующих данных. + +`LifecycleLoggingObserver` подписывается на вызовы методов жизненного цикла и логгирует номер строки, из которой был вызваны эти методы. + +Методы для навигации: + +* *pushViewController* добавляет `ViewController` в стек. Имеет два обязательных параметра *viewControllerClass* - класс, унаследованный от `ViewController` и *state* - объект описывающий состояние. + +* *pushViewControllerForResult* аналогичен предыдущему методу, используется, когда необходимо запустить какой-то фрагмент и при его завершении получить код. Для этого передаются еще два параметра: *requestCode* - код, который нужно получить при закрытии фрагмента и *targetFragment* - фрагмент, который должен получить этот код. + +* *setViewControllerAsTop* работает так же как и *pushViewController* но еще добавляет в качестве *backStackName* тег `TOP_FRAGMENT_TAG_MARK`. При выполнении возврата с помощью метода `up` будет выполнен возврат данному фрагменту. + +* *setInitialViewController* очищает стек и добавляет туда переданный `ViewController`. + +`ViewControllerNavigation` является наследником класса `FragmentNavigation` и для возвратов необходимо использовать методы из родительского класса: + +* *back* - вернуться к фрагменту, который лежит ниже в стеке. +* *up* - вернуться к самому низу стека, если в стеке нет фрагментов, помеченных тегом `TOP_FRAGMENT_TAG_MARK`. Если есть, то выполнить возврат к нему. Имеет два необязательных параметра: *name* - имя класса до которого нужно сделать возврат, если он не будет найден, то будет произведен возврат к самому низу стека; *inclusive* - если установить этот флаг, то будет произведен возврат к самому низу стека несмотря на фрагменты с тегом `TOP_FRAGMENT_TAG_MARK`. Если будет установлен и *name* и *inclusive*, то будет произведен возврат к фрагменту, который стоит ниже фрагмента с переданным *name*. + +### Примеры + +Файл `MainActivity.kt` +```Kotlin +class MainActivity : BaseActivity() { + + private val screenNavigation by lazy { + ViewControllerNavigation( + this, + supportFragmentManager, + R.id.fragment_container + ) + } + + fun getNavigation() = screenNavigation + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + screenNavigation.setInitialViewController( + MainViewController::class.java, + MainScreenState(true) + ) + } + +} +``` + +Файл `MainViewController.kt` +```Kotlin +class MainViewController( + creationContext: CreationContext +) : ViewController( + creationContext, + R.layout.view_controller_main +) { + + private val button: View = findViewById(R.id.view_controller_main_button) + + init { + button.setOnClickListener { + activity.getNavigation().pushViewController( + TutorialViewController::class.java, + EmptyState + ) + } + } + +} +``` + +Файл `activity_main.xml` +```xml + + + + + + +``` + +### Рекомендации + +Рекомендуется делать состояния, которые передаются во `ViewController` неизменяемыми, чтобы при навигации обратно `ViewController` корректно восстанавливались с изначально заданным состоянием. + +### Зависимости + +Для работы с данным модулем необходимо так же подключить модуль [logging](https://github.com/TouchInstinct/RoboSwag/tree/master/logging). + +```gradle +implementation project(':logging') +``` diff --git a/navigation-new/build.gradle b/navigation-new/build.gradle new file mode 100644 index 0000000..8d3b8e9 --- /dev/null +++ b/navigation-new/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion 16 + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + api project(":utils") + api project(":logging") + api project(":api-logansquare") + + api 'androidx.multidex:multidex:2.0.1' + + api 'net.danlew:android.joda:2.10.2' + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation "androidx.appcompat:appcompat:$versions.appcompat" + + implementation "androidx.fragment:fragment:$versions.fragment" + implementation "androidx.fragment:fragment-ktx:$versions.fragment" + + implementation "com.jakewharton:butterknife:$versions.butterknife" + kapt "com.jakewharton:butterknife-compiler:$versions.butterknife" + + implementation("com.crashlytics.sdk.android:crashlytics:$versions.crashlytics@aar") { + transitive = true + } +} diff --git a/navigation-new/src/main/AndroidManifest.xml b/navigation-new/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bd2d3ee --- /dev/null +++ b/navigation-new/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt new file mode 100644 index 0000000..2205dcc --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.components.navigation + +import android.content.Context +import android.os.Bundle +import android.os.Parcelable +import android.view.MenuItem +import androidx.annotation.IdRes +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import ru.touchin.roboswag.core.log.Lc +import ru.touchin.roboswag.components.navigation.fragments.BaseFragment +import ru.touchin.roboswag.components.navigation.viewcontrollers.EmptyState +import kotlin.reflect.KClass + +/** + * Created by Gavriil Sitnikov on 07/03/2016. + * Navigation which is controlling fragments on activity using [FragmentManager]. + * Basically there are 4 main actions to add fragments to activity. + * 1) [.setInitial] means to set fragment on top and remove all previously added fragments from stack; + * 2) [.push] means to simply add fragment on top of the stack; + * 3) [.setAsTop] means to push fragment on top of the stack with specific [.TOP_FRAGMENT_TAG_MARK] tag. + * It is useful to realize up/back navigation: if [.up] method will be called then stack will go to nearest fragment with TOP tag. + * If [.back] method will be called then stack will go to previous fragment. + * Usually such logic using to set as top fragments from sidebar and show hamburger when some of them appeared; + * 4) [.pushForResult] means to push fragment with target fragment. It is also adding [.WITH_TARGET_FRAGMENT_TAG_MARK] tag. + * Also if such up/back navigation logic is not OK then [.backTo] method could be used with any condition to back to. + * In that case in any stack-change method it is allowed to setup fragment transactions. + */ +open class FragmentNavigation( + private val context: Context, + private val fragmentManager: FragmentManager, + @IdRes private val containerViewId: Int, + private val transition: Int = FragmentTransaction.TRANSIT_FRAGMENT_OPEN +) { + + companion object { + const val TOP_FRAGMENT_TAG_MARK = "TOP_FRAGMENT" + } + + /** + * Returns if last fragment in stack is top (added by [.setAsTop] or [.setInitial]) like fragment from sidebar menu. + * + * @return True if last fragment on stack has TOP_FRAGMENT_TAG_MARK. + */ + fun isCurrentFragmentTop(): Boolean = if (fragmentManager.backStackEntryCount == 0) { + true + } else { + fragmentManager + .getBackStackEntryAt(fragmentManager.backStackEntryCount - 1) + .name + ?.contains(TOP_FRAGMENT_TAG_MARK) ?: false + } + + /** + * Allowed to react on [android.app.Activity]'s menu item selection. + * + * @param item Selected menu item; + * @return True if reaction fired. + */ + fun onOptionsItemSelected(item: MenuItem): Boolean = item.itemId == android.R.id.home && back() + + /** + * Base method which is adding fragment to stack. + * + * @param fragmentClass Class of [Fragment] to instantiate; + * @param targetFragment Target fragment to be set as [Fragment.getTargetFragment] of instantiated [Fragment]; + * @param addToStack Flag to add this transaction to the back stack; + * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; + * @param backStackName Name of [Fragment] in back stack; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + fun addToStack( + fragmentClass: Class, + targetFragment: Fragment?, + targetRequestCode: Int, + addToStack: Boolean, + args: Bundle?, + backStackName: String?, + transactionSetup: ((FragmentTransaction) -> Unit)? + ) { + if (fragmentManager.isDestroyed) { + Lc.assertion("FragmentManager is destroyed") + return + } + + val fragment = Fragment.instantiate(context, fragmentClass.name, args) + fragment.setTargetFragment(targetFragment, targetRequestCode) + + val fragmentTransaction = fragmentManager.beginTransaction() + transactionSetup?.invoke(fragmentTransaction) + fragmentTransaction.replace(containerViewId, fragment, null) + if (addToStack) { + fragmentTransaction + .addToBackStack(backStackName) + .setTransition(transition) + } + fragmentTransaction + .setPrimaryNavigationFragment(fragment) + .commit() + } + + /** + * Simply calls [FragmentManager.popBackStack]. + * + * @return True if it have back to some entry in stack. + */ + fun back(): Boolean { + if (fragmentManager.backStackEntryCount >= 1) { + fragmentManager.popBackStack() + return true + } + return false + } + + /** + * Backs to fragment with specific [.TOP_FRAGMENT_TAG_MARK] tag. + * This tag is adding if fragment added to stack via [.setInitial] or [.setAsTop] methods. + * It can be used to create simple up/back navigation. + * + * @return True if it have back to some entry in stack. + */ + fun up(name: String? = null, inclusive: Boolean = false) { + fragmentManager.popBackStack(name, if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0) + } + + /** + * Pushes [Fragment] on top of stack with specific arguments and transaction setup. + * + * @param fragmentClass Class of [Fragment] to instantiate; + * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + fun push( + fragmentClass: Class, + args: Bundle? = null, + addToStack: Boolean = true, + backStackName: String? = null, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + addToStack(fragmentClass, null, 0, addToStack, args, backStackName, transactionSetup) + } + + /** + * Pushes [Fragment] on top of stack with specific target fragment, arguments and transaction setup. + * + * @param fragmentClass KClass of [Fragment] to instantiate; + * @param state State of instantiated [Fragment]; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + fun push( + fragmentClass: KClass>, + state: T? = null, + addToStack: Boolean = true, + backStackName: String? = null, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + push(fragmentClass.java, BaseFragment.args(state ?: EmptyState), addToStack, backStackName, transactionSetup) + } + + /** + * Pushes [Fragment] on top of stack with specific target fragment, arguments and transaction setup. + * + * @param fragmentClass Class of [Fragment] to instantiate; + * @param targetFragment Target fragment to be set as [Fragment.getTargetFragment] of instantiated [Fragment]; + * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + fun pushForResult( + fragmentClass: Class, + targetFragment: Fragment, + targetRequestCode: Int, + args: Bundle? = null, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + addToStack( + fragmentClass, + targetFragment, + targetRequestCode, + true, + args, + null, + transactionSetup + ) + } + + /** + * Pushes [Fragment] on top of stack with specific target fragment, arguments and transaction setup. + * + * @param fragmentClass KClass of [Fragment] to instantiate; + * @param targetFragment Target fragment to be set as [Fragment.getTargetFragment] of instantiated [Fragment]; + * @param state State of instantiated [Fragment]; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + fun pushForResult( + fragmentClass: KClass>, + targetFragment: Fragment, + targetRequestCode: Int, + state: T? = null, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + pushForResult(fragmentClass.java, targetFragment, targetRequestCode, BaseFragment.args(state ?: EmptyState), transactionSetup) + } + + /** + * Pushes [Fragment] on top of stack with specific transaction setup, arguments + * and with [.TOP_FRAGMENT_TAG_MARK] tag used for simple up/back navigation. + * + * @param fragmentClass Class of [Fragment] to instantiate; + * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + fun setAsTop( + fragmentClass: Class, + args: Bundle? = null, + addToStack: Boolean = true, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + addToStack(fragmentClass, null, 0, addToStack, args, TOP_FRAGMENT_TAG_MARK, transactionSetup) + } + + /** + * Pops all [Fragment]s and places new initial [Fragment] on top of stack with specific transaction setup and arguments. + * + * @param fragmentClass Class of [Fragment] to instantiate; + * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + @JvmOverloads + fun setInitial( + fragmentClass: Class, + args: Bundle? = null, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + beforeSetInitialActions() + setAsTop(fragmentClass, args, false, transactionSetup) + } + + /** + * Pops all [Fragment]s and places new initial [Fragment] on top of stack with specific transaction setup and arguments. + * + * @param fragmentClass Class of [Fragment] to instantiate; + * @param state State of instantiated [Fragment]; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + fun setInitial( + fragmentClass: KClass>, + state: T? = null, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + beforeSetInitialActions() + setAsTop(fragmentClass.java, BaseFragment.args(state ?: EmptyState), false, transactionSetup) + } + + /** + * Method calls every time before initial [Fragment] will be placed. + */ + protected fun beforeSetInitialActions() { + if (fragmentManager.isDestroyed) { + Lc.assertion("FragmentManager is destroyed") + return + } + + if (fragmentManager.backStackEntryCount > 0) { + fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } + } + +} diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.kt new file mode 100644 index 0000000..79d417c --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.kt @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.components.navigation + +import android.animation.ValueAnimator +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.ActionBarDrawerToggle +import androidx.drawerlayout.widget.DrawerLayout +import androidx.fragment.app.FragmentManager +import ru.touchin.roboswag.components.navigation.activities.BaseActivity +import ru.touchin.roboswag.components.navigation.activities.OnBackPressedListener +import ru.touchin.roboswag.components.utils.UiUtils + +/** + * Created by Gavriil Sitnikov on 11/03/16. + * Simple realization of one-side [ActionBarDrawerToggle]. + */ +class SimpleActionBarDrawerToggle( + private val activity: BaseActivity, + val drawerLayout: DrawerLayout, + private val sidebar: View +) : ActionBarDrawerToggle(activity, drawerLayout, 0, 0), FragmentManager.OnBackStackChangedListener, OnBackPressedListener { + + private var isInvalidateOptionsMenuSupported = true + + private var hamburgerShowed: Boolean = false + private var sidebarDisabled: Boolean = false + + private var slideOffset: Float = 0f + private var slidePosition: Float = 0f + + private var hamburgerAnimator: ValueAnimator? = null + private var firstAnimation = true + + init { + drawerLayout.addDrawerListener(this) + activity.supportFragmentManager.addOnBackStackChangedListener(this) + activity.addOnBackPressedListener(this) + } + + /** + * Set turn on/off invocation of supportInvalidateOptionsMenu + * + * @param isInvalidateOptionsMenuSupported flag for turning on/off invocation. + */ + fun setInvalidateOptionsMenuSupported(isInvalidateOptionsMenuSupported: Boolean) { + this.isInvalidateOptionsMenuSupported = isInvalidateOptionsMenuSupported + } + + /** + * Returns if sidebar is opened. + * + * @return True if sidebar is opened. + */ + fun isOpened(): Boolean = drawerLayout.isDrawerOpen(sidebar) + + /** + * Disables sidebar. So it will be in closed state and couldn't be opened. + */ + fun disableSidebar() { + sidebarDisabled = true + close() + update() + } + + /** + * Enables sidebar. So it could be opened. + */ + fun enableSidebar() { + sidebarDisabled = false + update() + } + + /** + * Hides hamburger icon. Use it if there are some fragments in activity's stack. + */ + fun hideHamburger() { + syncState() + hamburgerShowed = true + update() + } + + /** + * Shows hamburger icon. Use it if there are no fragments in activity's stack or current fragment is like top. + */ + fun showHamburger() { + syncState() + hamburgerShowed = false + update() + } + + /** + * Opens sidebar. + */ + fun open() { + if (!sidebarDisabled && !drawerLayout.isDrawerOpen(sidebar)) { + drawerLayout.openDrawer(sidebar) + } + } + + /** + * Closes sidebar. + */ + fun close() { + if (drawerLayout.isDrawerOpen(sidebar)) { + drawerLayout.closeDrawer(sidebar) + } + } + + /** + * Method to process clicking on hamburger. It is needed to be called from [android.app.Activity.onOptionsItemSelected]. + * If this method won't be called then opening-closing won't work. + * + * @param item Selected item. + * @return True if item clicking processed. + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean = shouldShowHamburger() && super.onOptionsItemSelected(item) + + /** + * Call it when back stack of activity's fragments have changed. + */ + override fun onBackStackChanged() { + close() + } + + /** + * Call it when system back button have pressed. + */ + override fun onBackPressed(): Boolean = if (isOpened()) { + close() + true + } else { + false + } + + override fun onDrawerClosed(view: View) { + if (isInvalidateOptionsMenuSupported) { + activity.invalidateOptionsMenu() + } + } + + override fun onDrawerSlide(drawerView: View, offset: Float) { + if (offset in slideOffset..slidePosition + || offset in slidePosition..slideOffset) { + slideOffset = offset + } + super.onDrawerSlide(drawerView, slideOffset) + } + + /** + * Call it at [android.app.Activity.onPostCreate]. + */ + override fun syncState() { + cancelAnimation() + super.syncState() + } + + override fun onDrawerOpened(drawerView: View) { + UiUtils.OfViews.hideSoftInput(activity) + if (isInvalidateOptionsMenuSupported) { + activity.invalidateOptionsMenu() + } + } + + private fun shouldShowHamburger(): Boolean = !hamburgerShowed && !sidebarDisabled + + private fun update() { + setHamburgerState(shouldShowHamburger()) + drawerLayout.setDrawerLockMode(if (sidebarDisabled) DrawerLayout.LOCK_MODE_LOCKED_CLOSED else DrawerLayout.LOCK_MODE_UNLOCKED) + } + + private fun setHamburgerState(showHamburger: Boolean) { + if (!firstAnimation) { + cancelAnimation() + hamburgerAnimator = ValueAnimator.ofFloat(slideOffset, if (showHamburger) 0f else 1f) + hamburgerAnimator!!.addUpdateListener { animation -> onDrawerSlide(drawerLayout, animation.animatedValue as Float) } + hamburgerAnimator!!.start() + } else { + slideOffset = if (showHamburger) 0f else 1f + onDrawerSlide(drawerLayout, slideOffset) + } + slidePosition = if (showHamburger) 0f else 1f + firstAnimation = false + } + + private fun cancelAnimation() { + hamburgerAnimator?.cancel() + } + +} diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java new file mode 100644 index 0000000..49f4047 --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.components.navigation; + +import android.app.Application; +import android.content.Context; +import android.os.StrictMode; +import android.util.Log; + +import com.crashlytics.android.Crashlytics; + +import net.danlew.android.joda.JodaTimeAndroid; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.multidex.MultiDex; +import io.fabric.sdk.android.Fabric; +import ru.touchin.roboswag.core.log.ConsoleLogProcessor; +import ru.touchin.roboswag.core.log.Lc; +import ru.touchin.roboswag.core.log.LcGroup; +import ru.touchin.roboswag.core.log.LcLevel; +import ru.touchin.roboswag.core.log.LogProcessor; +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; +import ru.touchin.templates.ApiModel; + +/** + * Created by Gavriil Sitnikov on 10/03/16. + * Base class of application to extends for Touch Instinct related projects. + */ +public abstract class TouchinApp extends Application { + + @Override + protected void attachBaseContext(@NonNull final Context base) { + super.attachBaseContext(base); + MultiDex.install(base); + } + + @Override + public void onCreate() { + super.onCreate(); + JodaTimeAndroid.init(this); + if (BuildConfig.DEBUG) { + enableStrictMode(); + Lc.initialize(new ConsoleLogProcessor(LcLevel.VERBOSE), true); + LcGroup.UI_LIFECYCLE.disable(); + } else { + try { + final Crashlytics crashlytics = new Crashlytics(); + Fabric.with(this, crashlytics); + Fabric.getLogger().setLogLevel(Log.ERROR); + Lc.initialize(new CrashlyticsLogProcessor(crashlytics), false); + } catch (final NoClassDefFoundError error) { + Lc.initialize(new ConsoleLogProcessor(LcLevel.INFO), false); + Lc.e("Crashlytics initialization error! Did you forget to add\n" + + "compile('com.crashlytics.sdk.android:crashlytics:+@aar') {\n" + + " transitive = true;\n" + + "}\n" + + "to your build.gradle?", error); + } + } + } + + private void enableStrictMode() { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectAll() + .permitDiskReads() + .permitDiskWrites() + .penaltyLog() + .build()); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyLog() + .build()); + } + + private static class CrashlyticsLogProcessor extends LogProcessor { + + @NonNull + private final Crashlytics crashlytics; + + public CrashlyticsLogProcessor(@NonNull final Crashlytics crashlytics) { + super(LcLevel.INFO); + this.crashlytics = crashlytics; + } + + @Override + public void processLogMessage(@NonNull final LcGroup group, + @NonNull final LcLevel level, + @NonNull final String tag, + @NonNull final String message, + @Nullable final Throwable throwable) { + if (group == LcGroup.UI_LIFECYCLE) { + crashlytics.core.log(level.getPriority(), tag, message); + } else if (!level.lessThan(LcLevel.ASSERT) + || (group == ApiModel.API_VALIDATION_LC_GROUP && level == LcLevel.ERROR)) { + Log.e(tag, message); + if (throwable != null) { + crashlytics.core.log(level.getPriority(), tag, message); + crashlytics.core.logException(throwable); + } else { + final ShouldNotHappenException exceptionToLog = new ShouldNotHappenException(tag + ':' + message); + reduceStackTrace(exceptionToLog); + crashlytics.core.logException(exceptionToLog); + } + } + } + + private void reduceStackTrace(@NonNull final Throwable throwable) { + final StackTraceElement[] stackTrace = throwable.getStackTrace(); + final List reducedStackTraceList = new ArrayList<>(); + for (int i = stackTrace.length - 1; i >= 0; i--) { + final StackTraceElement stackTraceElement = stackTrace[i]; + if (stackTraceElement.getClassName().contains(getClass().getSimpleName()) + || stackTraceElement.getClassName().contains(LcGroup.class.getName()) + || stackTraceElement.getClassName().contains(Lc.class.getName())) { + break; + } + reducedStackTraceList.add(0, stackTraceElement); + } + StackTraceElement[] reducedStackTrace = new StackTraceElement[reducedStackTraceList.size()]; + reducedStackTrace = reducedStackTraceList.toArray(reducedStackTrace); + throwable.setStackTrace(reducedStackTrace); + } + + } + +} diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt new file mode 100644 index 0000000..1897cc1 --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.components.navigation.activities + +import android.content.Intent +import android.os.Bundle +import android.os.PersistableBundle +import androidx.appcompat.app.AppCompatActivity +import ru.touchin.roboswag.components.navigation.keyboard_resizeable.KeyboardBehaviorDetector +import ru.touchin.roboswag.components.navigation.viewcontrollers.LifecycleLoggingObserver +import ru.touchin.roboswag.core.log.Lc +import ru.touchin.roboswag.core.log.LcGroup + +/** + * Created by Gavriil Sitnikov on 08/03/2016. + * Base activity to use in components repository. + */ +abstract class BaseActivity : AppCompatActivity() { + + private val onBackPressedListeners = ArrayList() + + open val keyboardBehaviorDetector: KeyboardBehaviorDetector? = null + + init { + lifecycle.addObserver(LifecycleLoggingObserver()) + } + + override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { + super.onCreate(savedInstanceState, persistentState) + // Possible work around for market launches. See http://code.google.com/p/android/issues/detail?id=2373 + // for more details. Essentially, the market launches the main activity on top of other activities. + // we never want this to happen. Instead, we check if we are the root and if not, we finish. + if (!isTaskRoot && intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN == intent.action) { + Lc.e("Finishing activity as it is launcher but not root") + finish() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + LcGroup.UI_LIFECYCLE.i("${Lc.getCodePoint(this)} requestCode: $requestCode; resultCode: $resultCode") + } + + override fun onSaveInstanceState(stateToSave: Bundle) { + super.onSaveInstanceState(stateToSave) + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)) + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + open fun addOnBackPressedListener(onBackPressedListener: OnBackPressedListener) { + onBackPressedListeners.add(onBackPressedListener) + } + + open fun removeOnBackPressedListener(onBackPressedListener: OnBackPressedListener) { + onBackPressedListeners.remove(onBackPressedListener) + } + + open fun removeAllOnBackPressedListeners() { + onBackPressedListeners.clear() + } + + override fun onBackPressed() { + onBackPressedListeners.reversed().forEach { onBackPressedListener -> + if (onBackPressedListener.onBackPressed()) { + return + } + } + super.onBackPressed() + } + +} diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/activities/NavigationActivity.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/activities/NavigationActivity.kt new file mode 100644 index 0000000..51e7cfc --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/activities/NavigationActivity.kt @@ -0,0 +1,25 @@ +package ru.touchin.roboswag.components.navigation.activities + +import androidx.fragment.app.FragmentTransaction +import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewControllerNavigation + +/** + * Created by Daniil Borisovskii on 15/08/2019. + * Base activity with nested navigation. + */ +abstract class NavigationActivity : BaseActivity() { + + protected abstract val fragmentContainerViewId: Int + + protected open val transition = FragmentTransaction.TRANSIT_NONE + + open val navigation by lazy { + ViewControllerNavigation( + this, + supportFragmentManager, + fragmentContainerViewId, + transition + ) + } + +} diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java new file mode 100644 index 0000000..de5d318 --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java @@ -0,0 +1,7 @@ +package ru.touchin.roboswag.components.navigation.activities; + +public interface OnBackPressedListener { + + boolean onBackPressed(); + +} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/BaseFragment.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/fragments/BaseFragment.kt similarity index 100% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/BaseFragment.kt rename to navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/fragments/BaseFragment.kt diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.kt new file mode 100644 index 0000000..21a2f33 --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.kt @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.components.navigation.fragments + +import android.animation.Animator +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.animation.Animation +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle +import ru.touchin.roboswag.components.navigation.BuildConfig +import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController + +/** + * Created by Gavriil Sitnikov on 21/10/2015. + * Fragment instantiated in specific activity of [TActivity] type that is holding [ViewController] inside. + * + * @param Type of object which is representing it's fragment state; + * @param Type of [FragmentActivity] where fragment could be attached to. + */ +@Suppress("detekt.TooManyFunctions", "UNCHECKED_CAST") +open class ViewControllerFragment : Fragment() { + + companion object { + + private const val VIEW_CONTROLLER_CLASS_EXTRA = "VIEW_CONTROLLER_CLASS_EXTRA" + private const val VIEW_CONTROLLER_STATE_EXTRA = "VIEW_CONTROLLER_STATE_EXTRA" + + /** + * Creates [Bundle] which will store state. + * + * @param state State to use into ViewController. + * @return Returns bundle with state inside. + */ + fun args(viewControllerClass: Class>, state: Parcelable?): Bundle = Bundle().apply { + putSerializable(VIEW_CONTROLLER_CLASS_EXTRA, viewControllerClass) + putParcelable(VIEW_CONTROLLER_STATE_EXTRA, state) + } + + private fun reserialize(parcelable: T, classLoader: ClassLoader): T { + var parcel = Parcel.obtain() + parcel.writeParcelable(parcelable, 0) + val serializableBytes = parcel.marshall() + parcel.recycle() + parcel = Parcel.obtain() + parcel.unmarshall(serializableBytes, 0, serializableBytes.size) + parcel.setDataPosition(0) + val result = parcel.readParcelable(classLoader) ?: throw IllegalStateException("It must not be null") + parcel.recycle() + return result + } + } + + lateinit var state: TState private set + + lateinit var viewControllerClass: Class> private set + + private var viewController: ViewController? = null + + private var pendingActivityResult: ActivityResult? = null + + private var appeared: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setHasOptionsMenu(true) + + viewControllerClass = arguments?.getSerializable(VIEW_CONTROLLER_CLASS_EXTRA) as? Class> + ?: throw IllegalArgumentException("View controller class must be not-null") + + state = savedInstanceState?.getParcelable(VIEW_CONTROLLER_STATE_EXTRA) + ?: arguments?.getParcelable(VIEW_CONTROLLER_STATE_EXTRA) + ?: throw IllegalStateException("State is required and null") + + if (BuildConfig.DEBUG) { + state = reserialize(state, state.javaClass.classLoader ?: Thread.currentThread().contextClassLoader) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val newViewController = createViewController( + ViewController.CreationContext(requireActivity(), this, inflater, container), + savedInstanceState + ) + viewController = newViewController + newViewController.onCreate() + return newViewController.view + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + val activityResult = pendingActivityResult + if (viewController != null && activityResult != null) { + viewController?.onActivityResult(activityResult.requestCode, activityResult.resultCode, activityResult.data) + pendingActivityResult = null + } + } + + override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? = + viewController?.onCreateAnimation(transit, enter, nextAnim) + + override fun onCreateAnimator(transit: Int, enter: Boolean, nextAnim: Int): Animator? = + viewController?.onCreateAnimator(transit, enter, nextAnim) + + override fun onViewStateRestored(savedInstanceState: Bundle?) { + super.onViewStateRestored(savedInstanceState) + viewController?.onViewStateRestored(savedInstanceState) + } + + @SuppressLint("RestrictedApi") + override fun onStart() { + super.onStart() + if (!appeared && isMenuVisible) { + onAppear() + } + viewController?.onStart() + } + + /** + * Called when fragment is moved in started state and it's [.isMenuVisible] sets to true. + * Usually it is indicating that user can't see fragment on screen and useful to track analytics events. + */ + private fun onAppear() { + appeared = true + viewController?.onAppear() + } + + override fun onResume() { + super.onResume() + viewController?.onResume() + } + + override fun onLowMemory() { + super.onLowMemory() + viewController?.onLowMemory() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + viewController?.onCreateOptionsMenu(menu, inflater) + } + + override fun onPrepareOptionsMenu(menu: Menu) { + viewController?.onPrepareOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + viewController?.onOptionsItemSelected(item) == true || super.onOptionsItemSelected(item) + + override fun onPause() { + super.onPause() + viewController?.onPause() + } + + override fun onSaveInstanceState(savedInstanceState: Bundle) { + super.onSaveInstanceState(savedInstanceState) + viewController?.onSaveInstanceState(savedInstanceState) + savedInstanceState.putParcelable(VIEW_CONTROLLER_STATE_EXTRA, state) + } + + /** + * Called when fragment is moved in stopped state or it's [.isMenuVisible] sets to false. + * Usually it is indicating that user can't see fragment on screen and useful to track analytics events. + */ + private fun onDisappear() { + appeared = false + viewController?.onDisappear() + } + + override fun onStop() { + if (appeared) { + onDisappear() + } + viewController?.onStop() + super.onStop() + } + + override fun onDestroyView() { + viewController?.onDestroy() + viewController = null + super.onDestroyView() + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + viewController?.onRequestPermissionsResult(requestCode, permissions, grantResults) + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + viewController?.onActivityResult(requestCode, resultCode, data) ?: let { + pendingActivityResult = ActivityResult(requestCode, resultCode, data) + } + + } + + override fun setMenuVisibility(menuVisible: Boolean) { + super.setMenuVisibility(menuVisible) + if (activity != null && view != null) { + val started = lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) + if (!appeared && menuVisible && started) { + onAppear() + } + if (appeared && (!menuVisible || !started)) { + onDisappear() + } + } + } + + private fun createViewController( + creationContext: ViewController.CreationContext, + savedInstanceState: Bundle? + ): ViewController { + if (viewControllerClass.constructors.size != 1) { + throw IllegalStateException("There should be single constructor for $viewControllerClass") + } + val constructor = viewControllerClass.constructors[0] + return when (constructor.parameterTypes.size) { + 1 -> constructor.newInstance(creationContext) + 2 -> constructor.newInstance(creationContext, savedInstanceState) + else -> throw IllegalArgumentException("Wrong constructor parameters count: ${constructor.parameterTypes.size}") + } as ViewController + } + + override fun toString(): String = "${super.toString()} ViewController: $viewControllerClass" + + private data class ActivityResult(val requestCode: Int, val resultCode: Int, val data: Intent?) + +} diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt new file mode 100644 index 0000000..5001531 --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt @@ -0,0 +1,67 @@ +package ru.touchin.roboswag.components.navigation.keyboard_resizeable + +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup +import ru.touchin.roboswag.components.navigation.activities.BaseActivity + +// The workaround forces an activity to resize when keyboard appears in the full-screen mode +class KeyboardBehaviorDetector( + activity: BaseActivity, + fragmentContainerId: Int +) { + + companion object { + private const val SCREEN_TO_KEYBOARD_HEIGHT_RATIO = 4.75 + } + + private val contentContainer = activity.findViewById(android.R.id.content) as ViewGroup + private val fragmentContainer = activity.findViewById(fragmentContainerId) as ViewGroup + private lateinit var rootView: View + private val listener = { possiblyResizeChildOfContent() } + + private var keyboardHideListener: (() -> Unit)? = null + private var keyboardShowListener: ((Int) -> Unit)? = null + + fun setKeyboardHideListener(listener: () -> Unit) { + keyboardHideListener = listener + } + + fun removeKeyboardHideListener() { + keyboardHideListener = null + } + + fun setKeyboardShowListener(listener: (Int) -> Unit) { + keyboardShowListener = listener + } + + fun removeKeyboardShowListener() { + keyboardShowListener = null + } + + // Call this in "onResume()" of a fragment + fun startDetection() { + rootView = fragmentContainer.getChildAt(0) + + contentContainer.viewTreeObserver.addOnGlobalLayoutListener(listener) + } + + // Call this in "onPause()" of a fragment + fun stopDetection() { + contentContainer.viewTreeObserver.removeOnGlobalLayoutListener(listener) + } + + //https://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android?rq=1 + private fun possiblyResizeChildOfContent() { + val rect = Rect() + rootView.getWindowVisibleDisplayFrame(rect) + val height = rootView.context.resources.displayMetrics.heightPixels + val diff = height - rect.bottom + + if (diff > rootView.rootView.height / SCREEN_TO_KEYBOARD_HEIGHT_RATIO) { + keyboardShowListener?.invoke(diff) + } else { + keyboardHideListener?.invoke() + } + } +} diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardResizeableViewController.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardResizeableViewController.kt new file mode 100644 index 0000000..2d794ac --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardResizeableViewController.kt @@ -0,0 +1,82 @@ +package ru.touchin.roboswag.components.navigation.keyboard_resizeable + +import android.os.Build +import android.os.Parcelable +import androidx.annotation.LayoutRes +import ru.touchin.roboswag.components.navigation.activities.BaseActivity +import ru.touchin.roboswag.components.navigation.activities.OnBackPressedListener +import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController +import ru.touchin.roboswag.components.utils.UiUtils + +abstract class KeyboardResizeableViewController( + @LayoutRes layoutRes: Int, + creationContext: CreationContext +) : ViewController( + creationContext, + layoutRes +) { + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { + creationContext.container?.requestApplyInsets() + } + } + + private var keyboardIsVisible: Boolean = false + + private val keyboardHideListener = OnBackPressedListener { + if (keyboardIsVisible) { + UiUtils.OfViews.hideSoftInput(activity) + true + } else { + false + } + } + + private var isHideKeyboardOnBackEnabled = false + + protected open fun onKeyboardShow(diff: Int = 0) {} + + protected open fun onKeyboardHide() {} + + protected fun hideKeyboardOnBackPressed() { + isHideKeyboardOnBackEnabled = true + } + + override fun onResume() { + super.onResume() + if (isHideKeyboardOnBackEnabled) activity.addOnBackPressedListener(keyboardHideListener) + } + + override fun onPause() { + super.onPause() + if (isHideKeyboardOnBackEnabled) activity.removeOnBackPressedListener(keyboardHideListener) + } + + override fun onStart() { + super.onStart() + activity.keyboardBehaviorDetector?.apply { + setKeyboardHideListener { + if (keyboardIsVisible) { + onKeyboardHide() + } + keyboardIsVisible = false + } + setKeyboardShowListener { diff -> + if (!keyboardIsVisible) { + onKeyboardShow(diff) + } + keyboardIsVisible = true + } + startDetection() + } + } + + override fun onStop() { + super.onStop() + activity.keyboardBehaviorDetector?.apply { + removeKeyboardHideListener() + removeKeyboardShowListener() + stopDetection() + } + } +} diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt new file mode 100644 index 0000000..12424bc --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt @@ -0,0 +1,19 @@ +package ru.touchin.roboswag.components.navigation.viewcontrollers + +import android.os.Parcel +import android.os.Parcelable + +object EmptyState : Parcelable { + + override fun writeToParcel(parcel: Parcel, flags: Int) = Unit + + override fun describeContents() = 0 + + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel) = EmptyState + + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + +} diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/LifecycleLoggingObserver.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/LifecycleLoggingObserver.kt new file mode 100644 index 0000000..82ffac4 --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/LifecycleLoggingObserver.kt @@ -0,0 +1,16 @@ +package ru.touchin.roboswag.components.navigation.viewcontrollers + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import ru.touchin.roboswag.core.log.Lc +import ru.touchin.roboswag.core.log.LcGroup + +class LifecycleLoggingObserver : LifecycleObserver { + + @OnLifecycleEvent(Lifecycle.Event.ON_ANY) + fun onAnyLifecycleEvent() { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)) + } + +} diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.kt new file mode 100644 index 0000000..878c226 --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.kt @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.components.navigation.viewcontrollers + +import android.animation.Animator +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.animation.Animation +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes +import androidx.annotation.LayoutRes +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentTransaction +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment +import ru.touchin.roboswag.components.utils.UiUtils + +/** + * Created by Gavriil Sitnikov on 21/10/2015. + * Class to control view of specific fragment, activity and application by logic bridge. + * + * @param Type of activity where such [ViewController] could be; + * @param Type of state; + */ +@Suppress("detekt.TooManyFunctions", "UNCHECKED_CAST") +open class ViewController( + creationContext: CreationContext, + @LayoutRes layoutRes: Int +) : LifecycleOwner { + + val activity: TActivity = creationContext.activity as TActivity + + val fragment: ViewControllerFragment = creationContext.fragment as ViewControllerFragment + + val state = fragment.state + + val view: View = creationContext.inflater.inflate(layoutRes, creationContext.container, false) + + init { + lifecycle.addObserver(LifecycleLoggingObserver()) + } + + override fun getLifecycle(): Lifecycle = fragment.viewLifecycleOwner.lifecycle + + /** + * Look for a child view with the given id. If this view has the given id, return this view. + * + * @param id The id to search for; + * @return The view that has the given id in the hierarchy. + */ + fun findViewById(@IdRes id: Int): T = view.findViewById(id) + + /** + * Return a localized, styled CharSequence from the application's package's + * default string table. + * + * @param resId Resource id for the CharSequence text + */ + fun getText(@StringRes resId: Int): CharSequence = activity.getText(resId) + + /** + * Return a localized string from the application's package's default string table. + * + * @param resId Resource id for the string + */ + fun getString(@StringRes resId: Int): String = activity.getString(resId) + + /** + * Return a localized formatted string from the application's package's default string table, substituting the format arguments as defined in + * [java.util.Formatter] and [java.lang.String.format]. + * + * @param resId Resource id for the format string + * @param formatArgs The format arguments that will be used for substitution. + */ + fun getString(@StringRes resId: Int, vararg formatArgs: Any): String = activity.getString(resId, *formatArgs) + + /** + * Return the color value associated with a particular resource ID. + * Starting in [android.os.Build.VERSION_CODES.M], the returned + * color will be styled for the specified Context's theme. + * + * @param resId The resource id to search for data; + * @return int A single color value in the form 0xAARRGGBB. + */ + @ColorInt + fun getColor(@ColorRes resId: Int): Int = ContextCompat.getColor(activity, resId) + + /** + * Returns a color state list associated with a particular resource ID. + * + * + * Starting in [android.os.Build.VERSION_CODES.M], the returned + * color state list will be styled for the specified Context's theme. + * + * @param resId The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @return A color state list, or `null` if the resource could not be resolved. + * @throws android.content.res.Resources.NotFoundException if the given ID + * does not exist. + */ + fun getColorStateList(@ColorRes resId: Int): ColorStateList? = ContextCompat.getColorStateList(activity, resId) + + /** + * Returns a drawable object associated with a particular resource ID. + * Starting in [android.os.Build.VERSION_CODES.LOLLIPOP], the + * returned drawable will be styled for the specified Context's theme. + * + * @param resId The resource id to search for data; + * @return Drawable An object that can be used to draw this resource. + */ + fun getDrawable(@DrawableRes resId: Int): Drawable? = ContextCompat.getDrawable(activity, resId) + + fun startActivity(intent: Intent) { + fragment.startActivity(intent) + } + + fun startActivityForResult(intent: Intent, requestCode: Int) { + fragment.startActivityForResult(intent, requestCode) + } + + /** + * Calls when activity configuring ActionBar, Toolbar, Sidebar etc. + * If it will be called or not depends on [Fragment.hasOptionsMenu] and [Fragment.isMenuVisible]. + * + * @param menu The options menu in which you place your items; + * @param inflater Helper to inflate menu items. + */ + open fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) = Unit + + /** + * Prepare the standard options menu to be displayed. This is + * called right before the menu is shown, every time it is shown. + * You can use this method to efficiently enable/disable items or otherwise + * dynamically modify the contents. + * + * @param menu The options menu as last shown or first initialized by onCreateOptionsMenu(). + * + * @see [Fragment.hasOptionsMenu] + * @see [onCreateOptionsMenu] + */ + open fun onPrepareOptionsMenu(menu: Menu?) = Unit + + /** + * Calls right after construction of [ViewController]. + * Happens at [ViewControllerFragment.onActivityCreated]. + */ + open fun onCreate() = Unit + + /** + * Called when a fragment loads an animation. Note that if + * [FragmentTransaction.setCustomAnimations] was called with + * [Animator] resources instead of [Animation] resources, `nextAnim` + * will be an animator resource. + * + * @param transit The value set in [FragmentTransaction.setTransition] or 0 if not + * set. + * @param enter `true` when the fragment is added/attached/shown or `false` when + * the fragment is removed/detached/hidden. + * @param nextAnim The resource set in + * [FragmentTransaction.setCustomAnimations], + * [FragmentTransaction.setCustomAnimations], or + * 0 if neither was called. The value will depend on the current operation. + */ + open fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? = null + + /** + * Called when a fragment loads an animator. This will be called when + * [.onCreateAnimation] returns null. Note that if + * [FragmentTransaction.setCustomAnimations] was called with + * [Animation] resources instead of [Animator] resources, `nextAnim` + * will be an animation resource. + * + * @param transit The value set in [FragmentTransaction.setTransition] or 0 if not + * set. + * @param enter `true` when the fragment is added/attached/shown or `false` when + * the fragment is removed/detached/hidden. + * @param nextAnim The resource set in + * [FragmentTransaction.setCustomAnimations], + * [FragmentTransaction.setCustomAnimations], or + * 0 if neither was called. The value will depend on the current operation. + */ + open fun onCreateAnimator(transit: Int, enter: Boolean, nextAnim: Int): Animator? = null + + /** + * Calls when [ViewController] saved state has been restored into the view hierarchy. + * Happens at [ViewControllerFragment.onViewStateRestored]. + */ + open fun onViewStateRestored(savedInstanceState: Bundle?) = Unit + + /** + * Calls when [ViewController] have started. + * Happens at [ViewControllerFragment.onStart]. + */ + open fun onStart() { + UiUtils.OfViews.hideSoftInput(view) + } + + /** + * Called when fragment is moved in started state and it's [.getFragment] sets to true. + * Usually it is indicating that user can't see fragment on screen and useful to track analytics events. + */ + open fun onAppear() = Unit + + /** + * Calls when [ViewController] have resumed. + * Happens at [ViewControllerFragment.onResume]. + */ + open fun onResume() = Unit + + /** + * Calls when [ViewController] have goes near out of memory state. + * Happens at [ViewControllerFragment.onLowMemory]. + */ + open fun onLowMemory() = Unit + + /** + * Calls when [ViewController] have paused. + * Happens at [ViewControllerFragment.onPause]. + */ + open fun onPause() = Unit + + /** + * Calls when [ViewController] should save it's state. + * Happens at [ViewControllerFragment.onSaveInstanceState]. + * Try not to use such method for saving state but use [ViewControllerFragment.state] from [.getFragment]. + */ + open fun onSaveInstanceState(savedInstanceState: Bundle) = Unit + + /** + * Called when fragment is moved in stopped state or it's [.getFragment] sets to false. + * Usually it is indicating that user can't see fragment on screen and useful to track analytics events. + */ + open fun onDisappear() = Unit + + /** + * Calls when [ViewController] have stopped. + * Happens at [ViewControllerFragment.onStop]. + */ + open fun onStop() = Unit + + /** + * Calls when [ViewController] have destroyed. + * Happens usually at [ViewControllerFragment.onDestroyView]. In some cases at [ViewControllerFragment.onDestroy]. + */ + open fun onDestroy() = Unit + + /** + * Calls when [ViewController] have requested permissions results. + * Happens at [ViewControllerFragment.onRequestPermissionsResult] ()}. + */ + open fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) = Unit + + /** + * Callback from parent fragment. + */ + open fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = Unit + + /** + * Similar to [ViewControllerFragment.onOptionsItemSelected]. + * + * @param item Selected menu item; + * @return True if selection processed. + */ + open fun onOptionsItemSelected(item: MenuItem): Boolean = false + + /** + * Helper class to simplify constructor override. + */ + data class CreationContext( + val activity: FragmentActivity, + val fragment: ViewControllerFragment<*, *>, + val inflater: LayoutInflater, + val container: ViewGroup? + ) + +} diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt new file mode 100644 index 0000000..aa88022 --- /dev/null +++ b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.components.navigation.viewcontrollers + +import android.content.Context +import android.os.Parcelable +import androidx.annotation.IdRes +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction + +import ru.touchin.roboswag.components.navigation.FragmentNavigation +import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment + +/** + * Created by Gavriil Sitnikov on 07/03/2016. + * Navigation based on [ViewController]s which are creating by [Fragment]s. + * So basically it is just [FragmentNavigation] where most of fragments should be inherited from [ViewControllerFragment]. + * + * @param TActivity Type of activity where [ViewController]s should be showed. + */ +open class ViewControllerNavigation( + context: Context, + fragmentManager: FragmentManager, + @IdRes containerViewId: Int, + transition: Int = FragmentTransaction.TRANSIT_FRAGMENT_OPEN +) : FragmentNavigation(context, fragmentManager, containerViewId, transition) { + + /** + * Pushes [ViewController] on top of stack with specific [ViewControllerFragment.getState] and with specific transaction setup. + * + * @param viewControllerClass Class of [ViewController] to be pushed; + * @param state [Parcelable] of [ViewController]'s fragment; + * @param addToStack Flag to add this transaction to the back stack; + * @param backStackName Name of [Fragment] in back stack; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; + * @param TState Type of state of fragment. + */ + fun pushViewController( + viewControllerClass: Class>, + state: TState, + addToStack: Boolean = true, + backStackName: String? = null, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + addToStack( + ViewControllerFragment::class.java, + null, + 0, + addToStack, + ViewControllerFragment.args(viewControllerClass, state), + backStackName, + transactionSetup + ) + } + + /** + * Pushes [ViewController] on top of stack with specific [ViewControllerFragment.getState] + * and with specific [TTargetFragment] and transaction setup. + * + * @param viewControllerClass Class of [ViewController] to be pushed; + * @param targetFragment [ViewControllerFragment] to be set as target; + * @param state [Parcelable] of [ViewController]'s fragment; + * @param backStackName Name of [Fragment] in back stack; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; + * @param TState Type of state of fragment; + * @param TTargetFragment Type of target fragment. + */ + fun pushViewControllerForResult( + viewControllerClass: Class>, + state: TState, + targetFragment: TTargetFragment, + targetRequestCode: Int, + backStackName: String? = null, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + addToStack( + ViewControllerFragment::class.java, + targetFragment, + targetRequestCode, + true, + ViewControllerFragment.args(viewControllerClass, state), + backStackName, + transactionSetup + ) + } + + /** + * Pushes [ViewController] on top of stack with specific [ViewControllerFragment.getState] and with specific transaction setup + * and with [.TOP_FRAGMENT_TAG_MARK] tag used for simple up/back navigation. + * + * @param viewControllerClass Class of [ViewController] to be pushed; + * @param state [Parcelable] of [ViewController]'s fragment; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; + * @param TState Type of state of fragment. + */ + fun setViewControllerAsTop( + viewControllerClass: Class>, + state: TState, + addToStack: Boolean = true, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + addToStack( + ViewControllerFragment::class.java, + null, + 0, + addToStack, + ViewControllerFragment.args(viewControllerClass, state), + TOP_FRAGMENT_TAG_MARK, + transactionSetup + ) + } + + /** + * Pops all [Fragment]s and places new initial [ViewController] on top of stack + * with specific [ViewControllerFragment.getState] and specific transaction setup. + * + * @param viewControllerClass Class of [ViewController] to be pushed; + * @param state [Parcelable] of [ViewController]'s fragment; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; + * @param TState Type of state of fragment. + */ + fun setInitialViewController( + viewControllerClass: Class>, + state: TState, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + beforeSetInitialActions() + setViewControllerAsTop(viewControllerClass, state, false, transactionSetup) + } + +} diff --git a/navigation/build.gradle b/navigation/build.gradle index 8d3b8e9..c4fa596 100644 --- a/navigation/build.gradle +++ b/navigation/build.gradle @@ -28,12 +28,6 @@ dependencies { implementation "androidx.appcompat:appcompat:$versions.appcompat" - implementation "androidx.fragment:fragment:$versions.fragment" - implementation "androidx.fragment:fragment-ktx:$versions.fragment" - - implementation "com.jakewharton:butterknife:$versions.butterknife" - kapt "com.jakewharton:butterknife-compiler:$versions.butterknife" - implementation("com.crashlytics.sdk.android:crashlytics:$versions.crashlytics@aar") { transitive = true }