Added navigation-new module

This commit is contained in:
alex 2019-09-12 13:57:07 +03:00
parent 24eca02b5d
commit 53ffc2d2fc
19 changed files with 1836 additions and 6 deletions

1
navigation-new/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

123
navigation-new/README.md Normal file
View File

@ -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<MainActivity>(
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<MainActivity, MainScreenState>(
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
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
```
### Рекомендации
Рекомендуется делать состояния, которые передаются во `ViewController` неизменяемыми, чтобы при навигации обратно `ViewController` корректно восстанавливались с изначально заданным состоянием.
### Зависимости
Для работы с данным модулем необходимо так же подключить модуль [logging](https://github.com/TouchInstinct/RoboSwag/tree/master/logging).
```gradle
implementation project(':logging')
```

View File

@ -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
}
}

View File

@ -0,0 +1,3 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="ru.touchin.roboswag.components.navigation"/>

View File

@ -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<out Fragment>,
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<out Fragment>,
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 <T: Parcelable> push(
fragmentClass: KClass<out BaseFragment<*, out T>>,
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<out Fragment>,
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 <T: Parcelable> pushForResult(
fragmentClass: KClass<out BaseFragment<*, out T>>,
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<out Fragment>,
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<out Fragment>,
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 <T: Parcelable> setInitial(
fragmentClass: KClass<out BaseFragment<*, out T>>,
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)
}
}
}

View File

@ -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()
}
}

View File

@ -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<StackTraceElement> 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);
}
}
}

View File

@ -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<OnBackPressedListener>()
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()
}
}

View File

@ -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<NavigationActivity>(
this,
supportFragmentManager,
fragmentContainerViewId,
transition
)
}
}

View File

@ -0,0 +1,7 @@
package ru.touchin.roboswag.components.navigation.activities;
public interface OnBackPressedListener {
boolean onBackPressed();
}

View File

@ -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 <TState> Type of object which is representing it's fragment state;
* @param <TActivity> Type of [FragmentActivity] where fragment could be attached to.
</TActivity></TState> */
@Suppress("detekt.TooManyFunctions", "UNCHECKED_CAST")
open class ViewControllerFragment<TActivity : FragmentActivity, TState : Parcelable> : 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<out ViewController<*, *>>, state: Parcelable?): Bundle = Bundle().apply {
putSerializable(VIEW_CONTROLLER_CLASS_EXTRA, viewControllerClass)
putParcelable(VIEW_CONTROLLER_STATE_EXTRA, state)
}
private fun <T : Parcelable> 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<T>(classLoader) ?: throw IllegalStateException("It must not be null")
parcel.recycle()
return result
}
}
lateinit var state: TState private set
lateinit var viewControllerClass: Class<ViewController<TActivity, TState>> private set
private var viewController: ViewController<out TActivity, out TState>? = 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<ViewController<TActivity, TState>>
?: throw IllegalArgumentException("View controller class must be not-null")
state = savedInstanceState?.getParcelable<TState>(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<String>, 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<out TActivity, out TState> {
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<out TActivity, out TState>
}
override fun toString(): String = "${super.toString()} ViewController: $viewControllerClass"
private data class ActivityResult(val requestCode: Int, val resultCode: Int, val data: Intent?)
}

View File

@ -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()
}
}
}

View File

@ -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<TActivity : BaseActivity, TState : Parcelable>(
@LayoutRes layoutRes: Int,
creationContext: CreationContext
) : ViewController<TActivity, TState>(
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()
}
}
}

View File

@ -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<EmptyState> {
override fun createFromParcel(parcel: Parcel) = EmptyState
override fun newArray(size: Int): Array<EmptyState?> = arrayOfNulls(size)
}
}

View File

@ -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))
}
}

View File

@ -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 <TActivity> Type of activity where such [ViewController] could be;
* @param <TState> Type of state;
</TState></TActivity> */
@Suppress("detekt.TooManyFunctions", "UNCHECKED_CAST")
open class ViewController<TActivity : FragmentActivity, TState : Parcelable>(
creationContext: CreationContext,
@LayoutRes layoutRes: Int
) : LifecycleOwner {
val activity: TActivity = creationContext.activity as TActivity
val fragment: ViewControllerFragment<out TActivity, out TState> = creationContext.fragment as ViewControllerFragment<out TActivity, out TState>
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 <T : View> 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<String>, 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?
)
}

View File

@ -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<TActivity : FragmentActivity>(
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 <TState : Parcelable> pushViewController(
viewControllerClass: Class<out ViewController<out TActivity, TState>>,
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 <TState : Parcelable, TTargetFragment : Fragment> pushViewControllerForResult(
viewControllerClass: Class<out ViewController<out TActivity, TState>>,
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 <TState : Parcelable> setViewControllerAsTop(
viewControllerClass: Class<out ViewController<out TActivity, TState>>,
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 <TState : Parcelable> setInitialViewController(
viewControllerClass: Class<out ViewController<out TActivity, TState>>,
state: TState,
transactionSetup: ((FragmentTransaction) -> Unit)? = null
) {
beforeSetInitialActions()
setViewControllerAsTop(viewControllerClass, state, false, transactionSetup)
}
}

View File

@ -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
}