Merge pull request #45 from TouchInstinct/feature/tabbar

Add module "tabbar-navigation"
This commit is contained in:
Даниил Борисовский 2019-08-15 20:04:13 +03:00 committed by GitHub
commit 38d8cd3bb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 378 additions and 88 deletions

View File

@ -38,6 +38,7 @@ ext {
rxJava : '2.2.2',
rxAndroid : '2.1.0',
crashlytics: '2.9.5',
location : '16.0.0'
location : '16.0.0',
coreKtx : '1.0.1'
]
}

View File

@ -1,8 +1,31 @@
package ru.touchin.extensions
import android.app.Activity
import android.app.ActivityManager
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
fun Activity.safeStartActivityForResult(intent: Intent, requestCode: Int, options: Bundle? = null, resolveFlags: Int = 0): Boolean =
packageManager.resolveActivity(intent, resolveFlags)?.let { startActivityForResult(intent, requestCode, options) } != null
/**
* Setup task description of application for Android 5.0 and later. It is showing when user opens task bar.
*
* @param label Name of application to show in task bar;
* @param iconRes Icon of application to show in task bar;
* @param primaryColorRes Color of application to show in task bar.
*/
fun Activity.setupTaskDescriptor(label: String, @DrawableRes iconRes: Int, @ColorRes primaryColorRes: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val taskDescription = ActivityManager.TaskDescription(
label,
iconRes,
ContextCompat.getColor(this, primaryColorRes)
)
setTaskDescription(taskDescription)
}
}

View File

@ -18,7 +18,7 @@ include ':lifecycle-rx'
include ':views'
include ':recyclerview-adapters'
include ':kotlin-extensions'
include ':templates'
include ':tabbar-navigation'
project(':utils').projectDir = new File(rootDir, 'utils')
project(':logging').projectDir = new File(rootDir, 'logging')
@ -30,4 +30,4 @@ project(':lifecycle-rx').projectDir = new File(rootDir, 'lifecycle-rx')
project(':views').projectDir = new File(rootDir, 'views')
project(':recyclerview-adapters').projectDir = new File(rootDir, 'recyclerview-adapters')
project(':kotlin-extensions').projectDir = new File(rootDir, 'kotlin-extensions')
project(':templates').projectDir = new File(rootDir, 'templates')
project(':tabbar-navigation').projectDir = new File(rootDir, 'tabbar-navigation')

View File

@ -17,9 +17,18 @@ android {
dependencies {
api project(":utils")
api project(":logging")
api project(":api-logansquare")
api 'androidx.multidex:multidex:2.0.1'
api 'net.danlew:android.joda:2.9.9.4'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "androidx.appcompat:appcompat:$versions.appcompat"
implementation("com.crashlytics.sdk.android:crashlytics:$versions.crashlytics@aar") {
transitive = true
}
}

View File

@ -17,14 +17,11 @@
*
*/
package ru.touchin.templates;
package ru.touchin.roboswag.components.navigation;
import android.app.Application;
import android.content.Context;
import android.os.StrictMode;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.multidex.MultiDex;
import android.util.Log;
import com.crashlytics.android.Crashlytics;
@ -34,6 +31,9 @@ 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;
@ -41,6 +41,7 @@ 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.

View File

@ -21,6 +21,7 @@ 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
@ -41,6 +42,17 @@ abstract class BaseActivity : AppCompatActivity() {
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")

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
val navigation by lazy {
ViewControllerNavigation<NavigationActivity>(
this,
supportFragmentManager,
fragmentContainerViewId,
transition
)
}
}

View File

@ -1,4 +1,5 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion versions.compileSdk
@ -14,18 +15,11 @@ android {
}
dependencies {
api project(":utils")
api project(":logging")
api project(":api-logansquare")
api project(":navigation")
api 'androidx.multidex:multidex:2.0.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api 'net.danlew:android.joda:2.9.9.4'
implementation "androidx.core:core-ktx:$versions.coreKtx"
implementation "androidx.appcompat:appcompat:$versions.appcompat"
implementation("com.crashlytics.sdk.android:crashlytics:$versions.crashlytics@aar") {
transitive = true
}
}

View File

@ -0,0 +1,2 @@
<manifest
package="ru.touchin.roboswag.components.tabbarnavigation"/>

View File

@ -0,0 +1,25 @@
package ru.touchin.roboswag.components.tabbarnavigation
import androidx.fragment.app.FragmentManager
import ru.touchin.roboswag.components.navigation.activities.NavigationActivity
import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewControllerNavigation
/**
* Created by Daniil Borisovskii on 15/08/2019.
* Activity to manage tab container navigation.
*/
abstract class BottomNavigationActivity : NavigationActivity() {
val innerNavigation by lazy {
getNavigationContainer(supportFragmentManager)?.navigation ?: navigation as ViewControllerNavigation<BottomNavigationActivity>
}
private fun getNavigationContainer(fragmentManager: FragmentManager?): NavigationContainerFragment? =
fragmentManager
?.primaryNavigationFragment
?.let { navigationFragment ->
navigationFragment as? NavigationContainerFragment
?: getNavigationContainer(navigationFragment.childFragmentManager)
}
}

View File

@ -0,0 +1,123 @@
package ru.touchin.roboswag.components.tabbarnavigation
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import android.view.View
import android.view.ViewGroup
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.core.util.forEach
import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment
import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController
import ru.touchin.roboswag.core.utils.ShouldNotHappenException
class BottomNavigationController(
private val context: Context,
private val fragmentManager: FragmentManager,
private val viewControllers: SparseArray<Pair<Class<out ViewController<*, *>>, Parcelable>>,
@IdRes private val contentContainerViewId: Int,
@LayoutRes private val contentContainerLayoutId: Int,
private val wrapWithNavigationContainer: Boolean = false,
@IdRes private val topLevelViewControllerId: Int = 0, // If it zero back press with empty fragment back stack would close the app
private val onReselectListener: (() -> Unit)? = null
) {
private var callback: FragmentManager.FragmentLifecycleCallbacks? = null
private var currentViewControllerId = -1
fun attach(navigationTabsContainer: ViewGroup) {
detach()
//This is provides to set pressed tab status to isActivated providing an opportunity to specify custom style
callback = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(fragmentManager: FragmentManager, fragment: Fragment, view: View, savedInstanceState: Bundle?) {
viewControllers.forEach { itemId, (viewControllerClass, _) ->
if (isViewControllerFragment(fragment, viewControllerClass)) {
navigationTabsContainer.children.forEach { itemView -> itemView.isActivated = itemView.id == itemId }
}
}
}
}
fragmentManager.registerFragmentLifecycleCallbacks(callback!!, false)
navigationTabsContainer.children.forEach { itemView ->
viewControllers[itemView.id]?.let { (viewControllerClass, _) ->
itemView.setOnClickListener {
if (!isViewControllerFragment(fragmentManager.primaryNavigationFragment, viewControllerClass)) {
navigateTo(itemView.id)
} else {
onReselectListener?.invoke()
}
}
}
}
}
fun detach() = callback?.let(fragmentManager::unregisterFragmentLifecycleCallbacks)
fun navigateTo(@IdRes itemId: Int, state: Parcelable? = null) {
// Find view controller class that needs to open
val (viewControllerClass, defaultViewControllerState) = viewControllers[itemId] ?: return
if (state != null && state::class != defaultViewControllerState::class) {
throw ShouldNotHappenException(
"Incorrect state type for navigation tab root ViewController. Should be ${defaultViewControllerState::class}"
)
}
val viewControllerState = state ?: defaultViewControllerState
val transaction = fragmentManager.beginTransaction()
// Detach current primary fragment
fragmentManager.primaryNavigationFragment?.let(transaction::detach)
val viewControllerName = viewControllerClass.canonicalName
var fragment = fragmentManager.findFragmentByTag(viewControllerName)
if (fragment != null) {
transaction.attach(fragment)
} else {
fragment = if (wrapWithNavigationContainer) {
Fragment.instantiate(
context,
NavigationContainerFragment::class.java.name,
NavigationContainerFragment.args(viewControllerClass, viewControllerState, contentContainerViewId, contentContainerLayoutId)
)
} else {
Fragment.instantiate(
context,
ViewControllerFragment::class.java.name,
ViewControllerFragment.args(viewControllerClass, viewControllerState)
)
}
transaction.add(contentContainerViewId, fragment, viewControllerName)
}
transaction
.setPrimaryNavigationFragment(fragment)
.setReorderingAllowed(true)
.commit()
currentViewControllerId = itemId
}
// When you are in any tab instead of main you firstly navigate to main tab before exit application
fun onBackPressed() =
if (fragmentManager.primaryNavigationFragment?.childFragmentManager?.backStackEntryCount == 0
&& topLevelViewControllerId != 0
&& currentViewControllerId != topLevelViewControllerId) {
navigateTo(topLevelViewControllerId)
true
} else {
false
}
private fun isViewControllerFragment(fragment: Fragment?, viewControllerClass: Class<out ViewController<*, *>>) =
if (wrapWithNavigationContainer) {
(fragment as NavigationContainerFragment).getViewControllerClass()
} else {
(fragment as ViewControllerFragment<*, *>).viewControllerClass
} === viewControllerClass
}

View File

@ -0,0 +1,70 @@
package ru.touchin.roboswag.components.tabbarnavigation
import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import ru.touchin.roboswag.components.navigation.activities.OnBackPressedListener
import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController
abstract class BottomNavigationFragment : Fragment() {
private lateinit var bottomNavigationController: BottomNavigationController
private val backPressedListener = OnBackPressedListener { bottomNavigationController.onBackPressed() }
protected abstract val rootLayoutId: Int
protected abstract val navigationContainerViewId: Int
protected abstract val contentContainerViewId: Int
protected abstract val contentContainerLayoutId: Int
protected abstract val topLevelViewControllerId: Int
protected abstract val wrapWithNavigationContainer: Boolean
protected abstract val navigationViewControllers: SparseArray<Pair<Class<out ViewController<*, *>>, Parcelable>>
protected open val reselectListener: (() -> Unit) = { getNavigationActivity().innerNavigation.up() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bottomNavigationController = BottomNavigationController(
context = requireContext(),
fragmentManager = childFragmentManager,
viewControllers = navigationViewControllers,
contentContainerViewId = contentContainerViewId,
contentContainerLayoutId = contentContainerLayoutId,
topLevelViewControllerId = topLevelViewControllerId,
wrapWithNavigationContainer = wrapWithNavigationContainer,
onReselectListener = reselectListener
)
if (savedInstanceState == null) {
bottomNavigationController.navigateTo(topLevelViewControllerId)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val fragmentView = inflater.inflate(rootLayoutId, container, false)
bottomNavigationController.attach(fragmentView.findViewById(navigationContainerViewId))
(activity as BottomNavigationActivity).addOnBackPressedListener(backPressedListener)
return fragmentView
}
override fun onDestroyView() {
super.onDestroyView()
(activity as BottomNavigationActivity).removeOnBackPressedListener(backPressedListener)
bottomNavigationController.detach()
}
private fun getNavigationActivity() = requireActivity() as BottomNavigationActivity
}

View File

@ -0,0 +1,77 @@
package ru.touchin.roboswag.components.tabbarnavigation
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController
import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewControllerNavigation
import ru.touchin.roboswag.core.utils.ShouldNotHappenException
class NavigationContainerFragment : Fragment() {
companion object {
private const val VIEW_CONTROLLER_CLASS_ARG = "VIEW_CONTROLLER_CLASS_ARG"
private const val VIEW_CONTROLLER_STATE_ARG = "VIEW_CONTROLLER_STATE_ARG"
private const val CONTAINER_VIEW_ID_ARG = "CONTAINER_VIEW_ID_ARG"
private const val CONTAINER_LAYOUT_ID_ARG = "CONTAINER_LAYOUT_ID_ARG"
private const val TRANSITION_ARG = "TRANSITION_ARG"
fun args(
cls: Class<out ViewController<*, *>>,
state: Parcelable,
@IdRes containerViewId: Int,
@LayoutRes containerLayoutId: Int,
transition: Int = FragmentTransaction.TRANSIT_NONE
) = Bundle().apply {
putSerializable(VIEW_CONTROLLER_CLASS_ARG, cls)
putParcelable(VIEW_CONTROLLER_STATE_ARG, state)
putInt(CONTAINER_VIEW_ID_ARG, containerViewId)
putInt(CONTAINER_LAYOUT_ID_ARG, containerLayoutId)
putInt(TRANSITION_ARG, transition)
}
}
val navigation by lazy {
ViewControllerNavigation<BottomNavigationActivity>(
requireContext(),
childFragmentManager,
containerViewId,
transition
)
}
@IdRes
private var containerViewId = 0
@LayoutRes
private var containerLayoutId = 0
private var transition = 0
@Suppress("UNCHECKED_CAST")
fun getViewControllerClass(): Class<out ViewController<out BottomNavigationActivity, Parcelable>> =
arguments?.getSerializable(VIEW_CONTROLLER_CLASS_ARG) as Class<out ViewController<out BottomNavigationActivity, Parcelable>>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val args = arguments ?: throw ShouldNotHappenException("Fragment is not instantiable without arguments")
with(args) {
containerViewId = getInt(CONTAINER_VIEW_ID_ARG)
containerLayoutId = getInt(CONTAINER_LAYOUT_ID_ARG)
transition = getInt(TRANSITION_ARG)
}
navigation.setInitialViewController(getViewControllerClass(), args.getParcelable(VIEW_CONTROLLER_STATE_ARG))
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(containerLayoutId, container, false)
}

View File

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

View File

@ -1,69 +0,0 @@
/*
* 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.templates;
import android.app.ActivityManager;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import ru.touchin.roboswag.components.navigation.activities.BaseActivity;
import ru.touchin.roboswag.core.log.Lc;
/**
* Created by Gavriil Sitnikov on 11/03/16.
* Base class of activity to extends for Touch Instinct related projects.
*/
public abstract class TouchinActivity extends BaseActivity {
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 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() && getIntent().hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(getIntent().getAction())) {
Lc.e("Finishing activity as it is launcher but not root");
finish();
}
}
/**
* Setup task description of application for Android 5.0 and later. It is showing when user opens task bar.
*
* @param label Name of application to show in task bar;
* @param iconRes Icon of application to show in task bar;
* @param primaryColorRes Color of application to show in task bar.
*/
protected void setupTaskDescriptor(@NonNull final String label, @DrawableRes final int iconRes, @ColorRes final int primaryColorRes) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
final ActivityManager.TaskDescription taskDescription = new ActivityManager.TaskDescription(label,
iconRes,
ContextCompat.getColor(this, primaryColorRes));
setTaskDescription(taskDescription);
}
}
}