From c7f0346ff89912537b8de96aba1b119befa281e2 Mon Sep 17 00:00:00 2001 From: Phillip Thelen Date: Mon, 19 Nov 2018 18:13:35 +0100 Subject: [PATCH] Fix layout issues on devices with a notch --- Habitica/build.gradle | 3 +- Habitica/proguard-rules.pro | 2 +- .../bb_bottom_bar_item_container.xml | 24 + .../res/drawable/bb_bottom_bar_top_shadow.xml | 8 + Habitica/res/layout/activity_main.xml | 2 +- .../layout/bb_bottom_bar_item_container.xml | 31 + .../bb_bottom_bar_item_container_tablet.xml | 34 + .../res/layout/bb_bottom_bar_item_fixed.xml | 19 + .../bb_bottom_bar_item_fixed_tablet.xml | 14 + .../layout/bb_bottom_bar_item_shifting.xml | 26 + .../layout/bb_bottom_bar_item_titleless.xml | 10 + Habitica/res/layout/drawer_main.xml | 220 ++-- .../res/layout/fragment_avatar_overview.xml | 5 +- .../res/layout/fragment_challenge_detail.xml | 4 +- .../res/layout/fragment_challengeslist.xml | 4 - .../layout/fragment_equipment_overview.xml | 7 +- Habitica/res/layout/fragment_faq_detail.xml | 4 +- Habitica/res/layout/fragment_group_info.xml | 4 +- .../res/layout/fragment_guilds_overview.xml | 4 +- Habitica/res/layout/fragment_inbox.xml | 4 +- Habitica/res/layout/fragment_party_detail.xml | 4 +- Habitica/res/layout/fragment_quest_detail.xml | 4 +- Habitica/res/layout/fragment_skills.xml | 2 +- Habitica/res/layout/fragment_stats.xml | 4 +- .../res/layout/fragment_tavern_detail.xml | 4 +- .../res/layout/tavern_chat_new_entry_item.xml | 17 +- Habitica/res/values-v19/styles.xml | 4 +- Habitica/res/values/attrs.xml | 20 + Habitica/res/values/colors.xml | 4 + Habitica/res/values/dimens.xml | 3 + Habitica/res/values/ids.xml | 5 + Habitica/res/values/styles.xml | 32 + .../habitica/ui/activities/MainActivity.kt | 2 +- .../habitica/ui/fragments/BaseMainFragment.kt | 2 +- .../ui/fragments/NavigationDrawerFragment.kt | 17 +- .../habitica/ui/helpers/NavbarUtils.kt | 9 + .../ui/helpers/RecyclerViewEmptySupport.java | 72 -- .../ui/helpers/RecyclerViewEmptySupport.kt | 55 + .../habitica/ui/views/PaddedLinearLayout.kt | 26 + .../habitica/ui/views/PaddedRecylerView.kt | 41 + .../ui/views/bottombar/BadgeCircle.java | 58 + .../ui/views/bottombar/BadgeContainer.java | 14 + .../bottombar/BatchTabPropertyApplier.java | 26 + .../ui/views/bottombar/BottomBar.java | 1081 +++++++++++++++++ .../ui/views/bottombar/BottomBarBadge.java | 172 +++ .../ui/views/bottombar/BottomBarTab.java | 730 +++++++++++ .../bottombar/BottomNavigationBehavior.java | 177 +++ .../ui/views/bottombar/MiscUtils.java | 121 ++ .../bottombar/OnTabReselectListener.java | 30 + .../views/bottombar/OnTabSelectListener.java | 32 + .../ui/views/bottombar/ShySettings.java | 55 + .../ui/views/bottombar/TabParser.java | 210 ++++ .../bottombar/TabSelectionInterceptor.java | 33 + .../bottombar/VerticalScrollingBehavior.java | 150 +++ .../habitica/ui/views/social/ChatBarView.kt | 2 +- 55 files changed, 3413 insertions(+), 234 deletions(-) create mode 100644 Habitica/res/drawable-v21/bb_bottom_bar_item_container.xml create mode 100644 Habitica/res/drawable/bb_bottom_bar_top_shadow.xml create mode 100644 Habitica/res/layout/bb_bottom_bar_item_container.xml create mode 100644 Habitica/res/layout/bb_bottom_bar_item_container_tablet.xml create mode 100644 Habitica/res/layout/bb_bottom_bar_item_fixed.xml create mode 100644 Habitica/res/layout/bb_bottom_bar_item_fixed_tablet.xml create mode 100644 Habitica/res/layout/bb_bottom_bar_item_shifting.xml create mode 100644 Habitica/res/layout/bb_bottom_bar_item_titleless.xml create mode 100644 Habitica/res/values/ids.xml delete mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/RecyclerViewEmptySupport.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/RecyclerViewEmptySupport.kt create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/PaddedLinearLayout.kt create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/PaddedRecylerView.kt create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BadgeCircle.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BadgeContainer.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BatchTabPropertyApplier.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBar.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarBadge.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarTab.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomNavigationBehavior.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/MiscUtils.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/OnTabReselectListener.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/OnTabSelectListener.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/ShySettings.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/TabParser.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/TabSelectionInterceptor.java create mode 100644 Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/VerticalScrollingBehavior.java diff --git a/Habitica/build.gradle b/Habitica/build.gradle index 36576942c..440249242 100644 --- a/Habitica/build.gradle +++ b/Habitica/build.gradle @@ -61,7 +61,7 @@ dependencies { kapt 'com.google.dagger:dagger-compiler:2.17' compileOnly 'javax.annotation:javax.annotation-api:1.3.1' //App Compatibility and Material Design - implementation 'androidx.appcompat:appcompat:1.0.1' + implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'com.google.android.material:material:1.1.0-alpha01' implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'androidx.legacy:legacy-preference-v14:1.0.0' @@ -118,7 +118,6 @@ dependencies { implementation 'com.google.firebase:firebase-core:11.4.2' implementation 'com.google.firebase:firebase-messaging:11.4.2' implementation 'com.google.android.gms:play-services-auth:11.4.2' - implementation 'com.roughike:bottom-bar:2.3.1' implementation 'io.realm:android-adapters:3.0.0' implementation(project(':seeds-sdk')) { exclude group: 'com.google.android.gms' diff --git a/Habitica/proguard-rules.pro b/Habitica/proguard-rules.pro index 4930ab7e4..fc479c339 100644 --- a/Habitica/proguard-rules.pro +++ b/Habitica/proguard-rules.pro @@ -174,7 +174,7 @@ -dontwarn rx.** -dontwarn com.android.volley.toolbox.** -dontwarn com.facebook.infer.** --dontwarn com.roughike.bottombar.** +-dontwarn com.habitrpg.android.habitica.ui.views.bottombar.** -dontwarn com.viewpagerindicator.** #-ignorewarnings diff --git a/Habitica/res/drawable-v21/bb_bottom_bar_item_container.xml b/Habitica/res/drawable-v21/bb_bottom_bar_item_container.xml new file mode 100644 index 000000000..1185bd5fe --- /dev/null +++ b/Habitica/res/drawable-v21/bb_bottom_bar_item_container.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Habitica/res/drawable/bb_bottom_bar_top_shadow.xml b/Habitica/res/drawable/bb_bottom_bar_top_shadow.xml new file mode 100644 index 000000000..fd78efcf7 --- /dev/null +++ b/Habitica/res/drawable/bb_bottom_bar_top_shadow.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/Habitica/res/layout/activity_main.xml b/Habitica/res/layout/activity_main.xml index 0e8111ab1..2805bdb59 100644 --- a/Habitica/res/layout/activity_main.xml +++ b/Habitica/res/layout/activity_main.xml @@ -120,7 +120,7 @@ android:layout_gravity="bottom|right" android:layout_marginBottom="-5dp" /> - + + + + + + + + + + + + + \ No newline at end of file diff --git a/Habitica/res/layout/bb_bottom_bar_item_container_tablet.xml b/Habitica/res/layout/bb_bottom_bar_item_container_tablet.xml new file mode 100644 index 000000000..5ea8db66b --- /dev/null +++ b/Habitica/res/layout/bb_bottom_bar_item_container_tablet.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Habitica/res/layout/bb_bottom_bar_item_fixed.xml b/Habitica/res/layout/bb_bottom_bar_item_fixed.xml new file mode 100644 index 000000000..635148ada --- /dev/null +++ b/Habitica/res/layout/bb_bottom_bar_item_fixed.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/Habitica/res/layout/bb_bottom_bar_item_fixed_tablet.xml b/Habitica/res/layout/bb_bottom_bar_item_fixed_tablet.xml new file mode 100644 index 000000000..ea3ec7596 --- /dev/null +++ b/Habitica/res/layout/bb_bottom_bar_item_fixed_tablet.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/Habitica/res/layout/bb_bottom_bar_item_shifting.xml b/Habitica/res/layout/bb_bottom_bar_item_shifting.xml new file mode 100644 index 000000000..3337be4df --- /dev/null +++ b/Habitica/res/layout/bb_bottom_bar_item_shifting.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Habitica/res/layout/bb_bottom_bar_item_titleless.xml b/Habitica/res/layout/bb_bottom_bar_item_titleless.xml new file mode 100644 index 000000000..5c09509e2 --- /dev/null +++ b/Habitica/res/layout/bb_bottom_bar_item_titleless.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/Habitica/res/layout/drawer_main.xml b/Habitica/res/layout/drawer_main.xml index b39a75181..2f6918c37 100644 --- a/Habitica/res/layout/drawer_main.xml +++ b/Habitica/res/layout/drawer_main.xml @@ -1,118 +1,117 @@ + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/brand_200" + android:orientation="vertical" + tools:context="com.habitrpg.android.habitica.ui.fragments.NavigationDrawerFragment"> + + + + + - - - - - + - - - - - - - - - - - - - + android:ellipsize="end" + android:maxLines="1" + android:singleLine="true" + tools:text="Habitica" + style="@style/Body1" + android:textColor="@color/white"/> + + + + + + + + + + + + + android:layout_height="match_parent" + android:background="@color/white"/> diff --git a/Habitica/res/layout/fragment_avatar_overview.xml b/Habitica/res/layout/fragment_avatar_overview.xml index 9a7123402..6c7cb8f85 100644 --- a/Habitica/res/layout/fragment_avatar_overview.xml +++ b/Habitica/res/layout/fragment_avatar_overview.xml @@ -6,10 +6,9 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:scrollbarSize="3dp" android:scrollbarThumbVertical="@color/scrollbarThumb" - android:paddingBottom="?attr/actionBarSize" android:scrollbars="vertical"> - @@ -147,5 +146,5 @@ app:equipmentTitle="@string/avatar_background" /> - + diff --git a/Habitica/res/layout/fragment_challenge_detail.xml b/Habitica/res/layout/fragment_challenge_detail.xml index c25c53fb5..0c7c6cf07 100644 --- a/Habitica/res/layout/fragment_challenge_detail.xml +++ b/Habitica/res/layout/fragment_challenge_detail.xml @@ -6,7 +6,7 @@ android:layout_height="match_parent" android:orientation="vertical" app:maxHeightMultiplier="0.7"> - - + diff --git a/Habitica/res/layout/fragment_challengeslist.xml b/Habitica/res/layout/fragment_challengeslist.xml index 9f355ef17..483f3b94a 100644 --- a/Habitica/res/layout/fragment_challengeslist.xml +++ b/Habitica/res/layout/fragment_challengeslist.xml @@ -6,9 +6,6 @@ android:layout_height="match_parent" android:background="#fff" app:layout_behavior="@string/appbar_scrolling_view_behavior"> - - diff --git a/Habitica/res/layout/fragment_equipment_overview.xml b/Habitica/res/layout/fragment_equipment_overview.xml index 8b380c7af..68ca57e83 100644 --- a/Habitica/res/layout/fragment_equipment_overview.xml +++ b/Habitica/res/layout/fragment_equipment_overview.xml @@ -9,11 +9,10 @@ android:scrollbars="vertical" android:paddingTop="@dimen/row_padding"> - + android:orientation="vertical"> - + \ No newline at end of file diff --git a/Habitica/res/layout/fragment_faq_detail.xml b/Habitica/res/layout/fragment_faq_detail.xml index 154794348..a5538ad80 100644 --- a/Habitica/res/layout/fragment_faq_detail.xml +++ b/Habitica/res/layout/fragment_faq_detail.xml @@ -7,7 +7,7 @@ android:scrollbarThumbVertical="@color/scrollbarThumb" android:scrollbars="vertical"> - - + \ No newline at end of file diff --git a/Habitica/res/layout/fragment_group_info.xml b/Habitica/res/layout/fragment_group_info.xml index f6727eb7f..bd4d183b4 100644 --- a/Habitica/res/layout/fragment_group_info.xml +++ b/Habitica/res/layout/fragment_group_info.xml @@ -10,7 +10,7 @@ android:scrollbarSize="3dp" android:scrollbarThumbVertical="@color/scrollbarThumb" android:scrollbars="vertical"> - - + \ No newline at end of file diff --git a/Habitica/res/layout/fragment_guilds_overview.xml b/Habitica/res/layout/fragment_guilds_overview.xml index bd6c86864..e7672f198 100644 --- a/Habitica/res/layout/fragment_guilds_overview.xml +++ b/Habitica/res/layout/fragment_guilds_overview.xml @@ -9,7 +9,7 @@ - - + \ No newline at end of file diff --git a/Habitica/res/layout/fragment_inbox.xml b/Habitica/res/layout/fragment_inbox.xml index f144c2eb8..7288b931e 100644 --- a/Habitica/res/layout/fragment_inbox.xml +++ b/Habitica/res/layout/fragment_inbox.xml @@ -10,13 +10,13 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="?attr/actionBarSize"> - - + diff --git a/Habitica/res/layout/fragment_party_detail.xml b/Habitica/res/layout/fragment_party_detail.xml index ae4bbb3ed..8b1bf6512 100644 --- a/Habitica/res/layout/fragment_party_detail.xml +++ b/Habitica/res/layout/fragment_party_detail.xml @@ -8,7 +8,7 @@ - - + \ No newline at end of file diff --git a/Habitica/res/layout/fragment_quest_detail.xml b/Habitica/res/layout/fragment_quest_detail.xml index 82f203d6e..6e785a0b6 100644 --- a/Habitica/res/layout/fragment_quest_detail.xml +++ b/Habitica/res/layout/fragment_quest_detail.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> - - + \ No newline at end of file diff --git a/Habitica/res/layout/fragment_skills.xml b/Habitica/res/layout/fragment_skills.xml index e10ba38a8..046faf482 100644 --- a/Habitica/res/layout/fragment_skills.xml +++ b/Habitica/res/layout/fragment_skills.xml @@ -1,5 +1,5 @@ - - - + \ No newline at end of file diff --git a/Habitica/res/layout/fragment_tavern_detail.xml b/Habitica/res/layout/fragment_tavern_detail.xml index 7aa95b45e..92ed98b1c 100644 --- a/Habitica/res/layout/fragment_tavern_detail.xml +++ b/Habitica/res/layout/fragment_tavern_detail.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> - - + \ No newline at end of file diff --git a/Habitica/res/layout/tavern_chat_new_entry_item.xml b/Habitica/res/layout/tavern_chat_new_entry_item.xml index 18cbfdc86..8f6c2c4dc 100644 --- a/Habitica/res/layout/tavern_chat_new_entry_item.xml +++ b/Habitica/res/layout/tavern_chat_new_entry_item.xml @@ -2,7 +2,7 @@ - + android:layout_height="wrap_content"> + android:textColor="@color/brand_300" /> - + android:layout_height="match_parent" /> + \ No newline at end of file diff --git a/Habitica/res/values-v19/styles.xml b/Habitica/res/values-v19/styles.xml index 2b5cc6e5c..2586070a6 100644 --- a/Habitica/res/values-v19/styles.xml +++ b/Habitica/res/values-v19/styles.xml @@ -1,14 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt index 17743e486..b9b1fc92f 100755 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt @@ -67,7 +67,7 @@ import com.habitrpg.android.habitica.widget.AvatarStatsWidgetProvider import com.habitrpg.android.habitica.widget.DailiesWidgetProvider import com.habitrpg.android.habitica.widget.HabitButtonWidgetProvider import com.habitrpg.android.habitica.widget.TodoListWidgetProvider -import com.roughike.bottombar.BottomBar +import com.habitrpg.android.habitica.ui.views.bottombar.BottomBar import io.reactivex.Completable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.functions.Action diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/BaseMainFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/BaseMainFragment.kt index b7c65ca3d..15746caa0 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/BaseMainFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/BaseMainFragment.kt @@ -16,7 +16,7 @@ import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.helpers.SoundManager import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.ui.activities.MainActivity -import com.roughike.bottombar.BottomBar +import com.habitrpg.android.habitica.ui.views.bottombar.BottomBar import io.reactivex.functions.Consumer import javax.inject.Inject diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt index b6c348aec..0ce7cf8f1 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt @@ -3,6 +3,8 @@ package com.habitrpg.android.habitica.ui.fragments import android.app.ActionBar import android.content.Intent +import android.graphics.Rect +import android.os.Build import android.os.Bundle import androidx.fragment.app.DialogFragment import androidx.core.content.ContextCompat @@ -12,6 +14,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.Window import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.data.InventoryRepository @@ -47,6 +50,9 @@ import io.reactivex.disposables.CompositeDisposable import io.reactivex.functions.Consumer import kotlinx.android.synthetic.main.drawer_main.* import javax.inject.Inject +import android.view.Window.ID_ANDROID_CONTENT +import androidx.core.view.ViewCompat + /** * Fragment used for managing interactions for and presentation of a navigation drawer. @@ -166,10 +172,19 @@ class NavigationDrawerFragment : DialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.drawer_main, container, false) as ViewGroup + savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.drawer_main, container, false) as? ViewGroup override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + var statusBarHeight = 0 + val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") + if (resourceId > 0) { + statusBarHeight = resources.getDimensionPixelSize(resourceId) + } + val params = menuHeaderView.layoutParams as? ViewGroup.MarginLayoutParams + params?.topMargin = statusBarHeight + recyclerView.adapter = adapter recyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) initializeMenuItems() diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/NavbarUtils.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/NavbarUtils.kt index 650100ab4..3f17a26b7 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/NavbarUtils.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/NavbarUtils.kt @@ -34,6 +34,15 @@ object NavbarUtils { return size } + fun shouldDrawBehindNavbar(context: Context): Boolean { + return isPortrait(context) && hasSoftKeys(context) + } + + private fun isPortrait(context: Context): Boolean { + val res = context.resources + return res.getBoolean(R.bool.is_portrait_mode) + } + private fun getRealScreenSize(context: Context): Point { val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager val display = windowManager?.defaultDisplay diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/RecyclerViewEmptySupport.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/RecyclerViewEmptySupport.java deleted file mode 100644 index 68fb7cd46..000000000 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/RecyclerViewEmptySupport.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.habitrpg.android.habitica.ui.helpers; - -import android.content.Context; -import androidx.recyclerview.widget.RecyclerView; -import android.util.AttributeSet; -import android.view.View; -import android.view.WindowInsets; - -//http://stackoverflow.com/a/27801394/1315039 -public class RecyclerViewEmptySupport extends RecyclerView { - private View emptyView; - final private AdapterDataObserver observer = new AdapterDataObserver() { - @Override - public void onChanged() { - checkIfEmpty(); - } - - @Override - public void onItemRangeInserted(int positionStart, int itemCount) { - checkIfEmpty(); - } - - @Override - public void onItemRangeRemoved(int positionStart, int itemCount) { - checkIfEmpty(); - } - }; - - public RecyclerViewEmptySupport(Context context) { - super(context); - } - - public RecyclerViewEmptySupport(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public RecyclerViewEmptySupport(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - public WindowInsets onApplyWindowInsets(WindowInsets insets) { - return super.onApplyWindowInsets(insets); - } - - void checkIfEmpty() { - if (emptyView != null && getAdapter() != null) { - final boolean emptyViewVisible = getAdapter().getItemCount() == 0; - emptyView.setVisibility(emptyViewVisible ? VISIBLE : GONE); - setVisibility(emptyViewVisible ? GONE : VISIBLE); - } - } - - @Override - public void setAdapter(Adapter adapter) { - final Adapter oldAdapter = getAdapter(); - if (oldAdapter != null) { - oldAdapter.unregisterAdapterDataObserver(observer); - } - super.setAdapter(adapter); - if (adapter != null) { - adapter.registerAdapterDataObserver(observer); - } - - checkIfEmpty(); - } - - public void setEmptyView(View emptyView) { - this.emptyView = emptyView; - checkIfEmpty(); - } -} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/RecyclerViewEmptySupport.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/RecyclerViewEmptySupport.kt new file mode 100644 index 000000000..ec5ad5d30 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/RecyclerViewEmptySupport.kt @@ -0,0 +1,55 @@ +package com.habitrpg.android.habitica.ui.helpers + +import android.content.Context +import androidx.recyclerview.widget.RecyclerView +import android.util.AttributeSet +import android.view.View +import android.view.WindowInsets + +import com.habitrpg.android.habitica.ui.views.PaddedRecylerView + +//http://stackoverflow.com/a/27801394/1315039 +class RecyclerViewEmptySupport : PaddedRecylerView { + private var emptyView: View? = null + private val observer = object : RecyclerView.AdapterDataObserver() { + override fun onChanged() { + checkIfEmpty() + } + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + checkIfEmpty() + } + + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { + checkIfEmpty() + } + } + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + internal fun checkIfEmpty() { + if (emptyView != null && adapter != null) { + val emptyViewVisible = adapter?.itemCount == 0 + emptyView?.visibility = if (emptyViewVisible) View.VISIBLE else View.GONE + visibility = if (emptyViewVisible) View.GONE else View.VISIBLE + } + } + + override fun setAdapter(adapter: RecyclerView.Adapter<*>?) { + val oldAdapter = getAdapter() + oldAdapter?.unregisterAdapterDataObserver(observer) + super.setAdapter(adapter) + adapter?.registerAdapterDataObserver(observer) + + checkIfEmpty() + } + + fun setEmptyView(emptyView: View?) { + this.emptyView = emptyView + checkIfEmpty() + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/PaddedLinearLayout.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/PaddedLinearLayout.kt new file mode 100644 index 000000000..b0aab2a0c --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/PaddedLinearLayout.kt @@ -0,0 +1,26 @@ +package com.habitrpg.android.habitica.ui.views + +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import com.habitrpg.android.habitica.ui.helpers.NavbarUtils + +open class PaddedLinearLayout : LinearLayout { + private var navBarAccountedHeightCalculated = false + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val navbarHeight = NavbarUtils.getNavbarHeight(context) + val params = layoutParams as? MarginLayoutParams + params?.setMargins(0, 0, 0, navbarHeight) + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/PaddedRecylerView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/PaddedRecylerView.kt new file mode 100644 index 000000000..a0b322daa --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/PaddedRecylerView.kt @@ -0,0 +1,41 @@ +package com.habitrpg.android.habitica.ui.views + +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.habitrpg.android.habitica.ui.helpers.NavbarUtils + +open class PaddedRecylerView : RecyclerView { + private var navBarAccountedHeightCalculated = false + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + super.onLayout(changed, l, t, r, b) + if (changed) { + resizeForDrawingUnderNavbar() + } + } + + //https://github.com/roughike/BottomBar/blob/master/bottom-bar/src/main/java/com/roughike/bottombar/BottomBar.java#L834 + private fun resizeForDrawingUnderNavbar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + val currentHeight = height + + if (currentHeight != 0 && !navBarAccountedHeightCalculated) { + navBarAccountedHeightCalculated = true + + val navbarHeight = NavbarUtils.getNavbarHeight(context) + setPadding(0, 0, 0, navbarHeight) + (parent as? View)?.invalidate() + } + } + } + +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BadgeCircle.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BadgeCircle.java new file mode 100644 index 000000000..095efa2a0 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BadgeCircle.java @@ -0,0 +1,58 @@ +/* + * BottomBar library for Android + * Copyright (c) 2016 Iiro Krankka (http://github.com/roughike). + * + * 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 com.habitrpg.android.habitica.ui.views.bottombar; + +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.OvalShape; +import androidx.annotation.ColorInt; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; + +/* + * BottomBar library for Android + * Copyright (c) 2016 Iiro Krankka (http://github.com/roughike). + * + * 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. + */ +class BadgeCircle { + /** + * Creates a new circle for the Badge background. + * + * @param size the width and height for the circle + * @param color the activeIconColor for the circle + * @return a nice and adorable circle. + */ + @NonNull + static ShapeDrawable make(@IntRange(from = 0) int size, @ColorInt int color) { + ShapeDrawable indicator = new ShapeDrawable(new OvalShape()); + indicator.setIntrinsicWidth(size); + indicator.setIntrinsicHeight(size); + indicator.getPaint().setColor(color); + return indicator; + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BadgeContainer.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BadgeContainer.java new file mode 100644 index 000000000..e7551c4be --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BadgeContainer.java @@ -0,0 +1,14 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +import android.content.Context; +import androidx.annotation.NonNull; +import android.widget.FrameLayout; + +/** + * Created by iiro on 29.8.2016. + */ +public class BadgeContainer extends FrameLayout { + public BadgeContainer(@NonNull Context context) { + super(context); + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BatchTabPropertyApplier.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BatchTabPropertyApplier.java new file mode 100644 index 000000000..3a3849ebe --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BatchTabPropertyApplier.java @@ -0,0 +1,26 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +import androidx.annotation.NonNull; + +class BatchTabPropertyApplier { + private final BottomBar bottomBar; + + interface TabPropertyUpdater { + void update(BottomBarTab tab); + } + + BatchTabPropertyApplier(@NonNull BottomBar bottomBar) { + this.bottomBar = bottomBar; + } + + void applyToAllTabs(@NonNull TabPropertyUpdater propertyUpdater) { + int tabCount = bottomBar.getTabCount(); + + if (tabCount > 0) { + for (int i = 0; i < tabCount; i++) { + BottomBarTab tab = bottomBar.getTabAtPosition(i); + propertyUpdater.update(tab); + } + } + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBar.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBar.java new file mode 100644 index 000000000..77bc8f0ce --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBar.java @@ -0,0 +1,1081 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import androidx.annotation.ColorInt; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.XmlRes; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.content.ContextCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.ViewPropertyAnimatorListenerAdapter; + +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.ViewParent; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.habitrpg.android.habitica.R; +import com.habitrpg.android.habitica.ui.helpers.NavbarUtils; + +import java.util.List; + +/* + * BottomBar library for Android + * Copyright (c) 2016 Iiro Krankka (http://github.com/roughike). + * + * 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. + */ +public class BottomBar extends LinearLayout implements View.OnClickListener, View.OnLongClickListener { + private static final String STATE_CURRENT_SELECTED_TAB = "STATE_CURRENT_SELECTED_TAB"; + private static final float DEFAULT_INACTIVE_SHIFTING_TAB_ALPHA = 0.6f; + // Behaviors + private static final int BEHAVIOR_NONE = 0; + private static final int BEHAVIOR_SHIFTING = 1; + private static final int BEHAVIOR_SHY = 2; + private static final int BEHAVIOR_DRAW_UNDER_NAV = 4; + private static final int BEHAVIOR_ICONS_ONLY = 8; + + private BatchTabPropertyApplier batchPropertyApplier; + private int primaryColor; + private int screenWidth; + private int tenDp; + private int maxFixedItemWidth; + + // XML Attributes + private int tabXmlResource; + private boolean isTabletMode; + private int behaviors; + private float inActiveTabAlpha; + private float activeTabAlpha; + private int inActiveTabColor; + private int activeTabColor; + private int badgeBackgroundColor; + private boolean hideBadgeWhenActive; + private boolean longPressHintsEnabled; + private int titleTextAppearance; + private Typeface titleTypeFace; + private boolean showShadow; + private float shadowElevation; + private View shadowView; + + private View backgroundOverlay; + private ViewGroup outerContainer; + private ViewGroup tabContainer; + + private int defaultBackgroundColor = Color.WHITE; + private int currentBackgroundColor; + private int currentTabPosition; + + private int inActiveShiftingItemWidth; + private int activeShiftingItemWidth; + + @Nullable + private TabSelectionInterceptor tabSelectionInterceptor; + + @Nullable + private OnTabSelectListener onTabSelectListener; + + @Nullable + private OnTabReselectListener onTabReselectListener; + + private boolean isComingFromRestoredState; + private boolean ignoreTabReselectionListener; + + private ShySettings shySettings; + private boolean shyHeightAlreadyCalculated; + private boolean navBarAccountedHeightCalculated; + + private BottomBarTab[] currentTabs; + + public BottomBar(Context context) { + this(context, null); + } + + public BottomBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BottomBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, 0); + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public BottomBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + batchPropertyApplier = new BatchTabPropertyApplier(this); + + populateAttributes(context, attrs, defStyleAttr, defStyleRes); + initializeViews(); + determineInitialBackgroundColor(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + init21(context); + } + + if (tabXmlResource != 0) { + setItems(tabXmlResource); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + // This is so that in Pre-Lollipop devices there is a shadow BUT without pushing the content + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && showShadow && shadowView != null) { + shadowView.setVisibility(VISIBLE); + ViewGroup.LayoutParams params = getLayoutParams(); + if (params instanceof MarginLayoutParams) { + MarginLayoutParams layoutParams = (MarginLayoutParams) params; + final int shadowHeight = getResources().getDimensionPixelSize(R.dimen.bb_fake_shadow_height); + + layoutParams.setMargins(layoutParams.leftMargin, + layoutParams.topMargin - shadowHeight, + layoutParams.rightMargin, + layoutParams.bottomMargin); + setLayoutParams(params); + } + } + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private void init21(Context context) { + if (showShadow) { + shadowElevation = getElevation(); + shadowElevation = shadowElevation > 0 + ? shadowElevation + : getResources().getDimensionPixelSize(R.dimen.bb_default_elevation); + setElevation(MiscUtils.dpToPixel(context, shadowElevation)); + setOutlineProvider(ViewOutlineProvider.BOUNDS); + } + } + + private void populateAttributes(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + primaryColor = MiscUtils.getColor(getContext(), R.attr.colorPrimary); + screenWidth = MiscUtils.getScreenWidth(getContext()); + tenDp = MiscUtils.dpToPixel(getContext(), 10); + maxFixedItemWidth = MiscUtils.dpToPixel(getContext(), 168); + + TypedArray ta = context.getTheme() + .obtainStyledAttributes(attrs, R.styleable.BottomBar, defStyleAttr, defStyleRes); + + try { + tabXmlResource = ta.getResourceId(R.styleable.BottomBar_bb_tabXmlResource, 0); + isTabletMode = ta.getBoolean(R.styleable.BottomBar_bb_tabletMode, false); + behaviors = ta.getInteger(R.styleable.BottomBar_bb_behavior, BEHAVIOR_NONE); + inActiveTabAlpha = ta.getFloat(R.styleable.BottomBar_bb_inActiveTabAlpha, + isShiftingMode() ? DEFAULT_INACTIVE_SHIFTING_TAB_ALPHA : 1); + activeTabAlpha = ta.getFloat(R.styleable.BottomBar_bb_activeTabAlpha, 1); + + @ColorInt + int defaultInActiveColor = isShiftingMode() ? + Color.WHITE : ContextCompat.getColor(context, R.color.bb_inActiveBottomBarItemColor); + int defaultActiveColor = isShiftingMode() ? Color.WHITE : primaryColor; + + longPressHintsEnabled = ta.getBoolean(R.styleable.BottomBar_bb_longPressHintsEnabled, true); + inActiveTabColor = ta.getColor(R.styleable.BottomBar_bb_inActiveTabColor, defaultInActiveColor); + activeTabColor = ta.getColor(R.styleable.BottomBar_bb_activeTabColor, defaultActiveColor); + badgeBackgroundColor = ta.getColor(R.styleable.BottomBar_bb_badgeBackgroundColor, Color.RED); + hideBadgeWhenActive = ta.getBoolean(R.styleable.BottomBar_bb_badgesHideWhenActive, true); + titleTextAppearance = ta.getResourceId(R.styleable.BottomBar_bb_titleTextAppearance, 0); + titleTypeFace = getTypeFaceFromAsset(ta.getString(R.styleable.BottomBar_bb_titleTypeFace)); + showShadow = ta.getBoolean(R.styleable.BottomBar_bb_showShadow, true); + } finally { + ta.recycle(); + } + } + + private boolean isShiftingMode() { + return !isTabletMode && hasBehavior(BEHAVIOR_SHIFTING); + } + + private boolean drawUnderNav() { + return !isTabletMode + && hasBehavior(BEHAVIOR_DRAW_UNDER_NAV) + && NavbarUtils.INSTANCE.shouldDrawBehindNavbar(getContext()); + } + + boolean isShy() { + return !isTabletMode && hasBehavior(BEHAVIOR_SHY); + } + + boolean isShyHeightAlreadyCalculated() { + return shyHeightAlreadyCalculated; + } + + private boolean isIconsOnlyMode() { + return !isTabletMode && hasBehavior(BEHAVIOR_ICONS_ONLY); + } + + private boolean hasBehavior(int behavior) { + return (behaviors | behavior) == behaviors; + } + + private Typeface getTypeFaceFromAsset(String fontPath) { + if (fontPath != null) { + return Typeface.createFromAsset( + getContext().getAssets(), fontPath); + } + + return null; + } + + private void initializeViews() { + int width = isTabletMode ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; + int height = isTabletMode ? LayoutParams.MATCH_PARENT : LayoutParams.WRAP_CONTENT; + LayoutParams params = new LayoutParams(width, height); + + setLayoutParams(params); + setOrientation(isTabletMode ? HORIZONTAL : VERTICAL); + + View rootView = inflate(getContext(), + isTabletMode ? R.layout.bb_bottom_bar_item_container_tablet : R.layout.bb_bottom_bar_item_container, this); + rootView.setLayoutParams(params); + + backgroundOverlay = rootView.findViewById(R.id.bb_bottom_bar_background_overlay); + outerContainer = (ViewGroup) rootView.findViewById(R.id.bb_bottom_bar_outer_container); + tabContainer = (ViewGroup) rootView.findViewById(R.id.bb_bottom_bar_item_container); + shadowView = findViewById(R.id.bb_bottom_bar_shadow); + } + + private void determineInitialBackgroundColor() { + if (isShiftingMode()) { + defaultBackgroundColor = primaryColor; + } + + Drawable userDefinedBackground = getBackground(); + + boolean userHasDefinedBackgroundColor = userDefinedBackground != null + && userDefinedBackground instanceof ColorDrawable; + + if (userHasDefinedBackgroundColor) { + defaultBackgroundColor = ((ColorDrawable) userDefinedBackground).getColor(); + setBackgroundColor(Color.TRANSPARENT); + } + } + + /** + * Set the items for the BottomBar from XML Resource. + */ + public void setItems(@XmlRes int xmlRes) { + setItems(xmlRes, null); + } + + /** + * Set the item for the BottomBar from XML Resource with a default configuration + * for each tab. + */ + public void setItems(@XmlRes int xmlRes, BottomBarTab.Config defaultTabConfig) { + if (xmlRes == 0) { + throw new RuntimeException("No items specified for the BottomBar!"); + } + + if (defaultTabConfig == null) { + defaultTabConfig = getTabConfig(); + } + + TabParser parser = new TabParser(getContext(), defaultTabConfig, xmlRes); + updateItems(parser.parseTabs()); + } + + private BottomBarTab.Config getTabConfig() { + return new BottomBarTab.Config.Builder() + .inActiveTabAlpha(inActiveTabAlpha) + .activeTabAlpha(activeTabAlpha) + .inActiveTabColor(inActiveTabColor) + .activeTabColor(activeTabColor) + .barColorWhenSelected(defaultBackgroundColor) + .badgeBackgroundColor(badgeBackgroundColor) + .hideBadgeWhenSelected(hideBadgeWhenActive) + .titleTextAppearance(titleTextAppearance) + .titleTypeFace(titleTypeFace) + .build(); + } + + private void updateItems(final List bottomBarItems) { + tabContainer.removeAllViews(); + + int index = 0; + int biggestWidth = 0; + + BottomBarTab[] viewsToAdd = new BottomBarTab[bottomBarItems.size()]; + + for (BottomBarTab bottomBarTab : bottomBarItems) { + BottomBarTab.Type type; + + if (isShiftingMode()) { + type = BottomBarTab.Type.SHIFTING; + } else if (isTabletMode) { + type = BottomBarTab.Type.TABLET; + } else { + type = BottomBarTab.Type.FIXED; + } + + if (isIconsOnlyMode()) { + bottomBarTab.setIsTitleless(true); + } + + bottomBarTab.setType(type); + bottomBarTab.prepareLayout(); + + if (index == currentTabPosition) { + bottomBarTab.select(false); + + handleBackgroundColorChange(bottomBarTab, false); + } else { + bottomBarTab.deselect(false); + } + + if (!isTabletMode) { + if (bottomBarTab.getWidth() > biggestWidth) { + biggestWidth = bottomBarTab.getWidth(); + } + + viewsToAdd[index] = bottomBarTab; + } else { + tabContainer.addView(bottomBarTab); + } + + bottomBarTab.setOnClickListener(this); + bottomBarTab.setOnLongClickListener(this); + index++; + } + + currentTabs = viewsToAdd; + + if (!isTabletMode) { + resizeTabsToCorrectSizes(viewsToAdd); + } + } + + private void resizeTabsToCorrectSizes(BottomBarTab[] tabsToAdd) { + int viewWidth = MiscUtils.pixelToDp(getContext(), getWidth()); + + if (viewWidth <= 0 || viewWidth > screenWidth) { + viewWidth = screenWidth; + } + + int proposedItemWidth = Math.min( + MiscUtils.dpToPixel(getContext(), viewWidth / tabsToAdd.length), + maxFixedItemWidth + ); + + inActiveShiftingItemWidth = (int) (proposedItemWidth * 0.9); + activeShiftingItemWidth = (int) (proposedItemWidth + (proposedItemWidth * ((tabsToAdd.length - 1) * 0.1))); + int height = Math.round(getContext().getResources() + .getDimension(R.dimen.bb_height)); + + for (BottomBarTab tabView : tabsToAdd) { + ViewGroup.LayoutParams params = tabView.getLayoutParams(); + params.height = height; + + if (isShiftingMode()) { + if (tabView.isActive()) { + params.width = activeShiftingItemWidth; + } else { + params.width = inActiveShiftingItemWidth; + } + } else { + params.width = proposedItemWidth; + } + + if (tabView.getParent() == null) { + tabContainer.addView(tabView); + } + + tabView.setLayoutParams(params); + } + } + + /** + * Returns the settings specific for a shy BottomBar. + * + * @throws UnsupportedOperationException, if this BottomBar is not shy. + */ + public ShySettings getShySettings() { + if (!isShy()) { + Log.e("BottomBar", "Tried to get shy settings for a BottomBar " + + "that is not shy."); + } + + if (shySettings == null) { + shySettings = new ShySettings(this); + } + + return shySettings; + } + + /** + * Set a listener that gets fired when the selected {@link BottomBarTab} is about to change. + * + * @param interceptor a listener for potentially interrupting changes in tab selection. + */ + public void setTabSelectionInterceptor(@NonNull TabSelectionInterceptor interceptor) { + tabSelectionInterceptor = interceptor; + } + + /** + * Removes the current {@link TabSelectionInterceptor} listener + */ + public void removeOverrideTabSelectionListener() { + tabSelectionInterceptor = null; + } + + /** + * Set a listener that gets fired when the selected {@link BottomBarTab} changes. + *

+ * Note: Will be immediately called for the currently selected tab + * once when set. + * + * @param listener a listener for monitoring changes in tab selection. + */ + public void setOnTabSelectListener(@NonNull OnTabSelectListener listener) { + setOnTabSelectListener(listener, true); + } + + /** + * Set a listener that gets fired when the selected {@link BottomBarTab} changes. + *

+ * If {@code shouldFireInitially} is set to false, this listener isn't fired straight away + * it's set, but you'll get all events normally for consecutive tab selection changes. + * + * @param listener a listener for monitoring changes in tab selection. + * @param shouldFireInitially whether the listener should be fired the first time it's set. + */ + public void setOnTabSelectListener(@NonNull OnTabSelectListener listener, boolean shouldFireInitially) { + onTabSelectListener = listener; + + if (shouldFireInitially && getTabCount() > 0) { + listener.onTabSelected(getCurrentTabId()); + } + } + + /** + * Removes the current {@link OnTabSelectListener} listener + */ + public void removeOnTabSelectListener() { + onTabSelectListener = null; + } + + /** + * Set a listener that gets fired when a currently selected {@link BottomBarTab} is clicked. + * + * @param listener a listener for handling tab reselections. + */ + public void setOnTabReselectListener(@NonNull OnTabReselectListener listener) { + onTabReselectListener = listener; + } + + /** + * Removes the current {@link OnTabReselectListener} listener + */ + public void removeOnTabReselectListener() { + onTabReselectListener = null; + } + + /** + * Set the default selected to be the tab with the corresponding tab id. + * By default, the first tab in the container is the default tab. + */ + public void setDefaultTab(@IdRes int defaultTabId) { + int defaultTabPosition = findPositionForTabWithId(defaultTabId); + setDefaultTabPosition(defaultTabPosition); + } + + /** + * Sets the default tab for this BottomBar that is shown until the user changes + * the selection. + * + * @param defaultTabPosition the default tab position. + */ + public void setDefaultTabPosition(int defaultTabPosition) { + if (isComingFromRestoredState) return; + + selectTabAtPosition(defaultTabPosition); + } + + /** + * Select the tab with the corresponding id. + */ + public void selectTabWithId(@IdRes int tabResId) { + int tabPosition = findPositionForTabWithId(tabResId); + selectTabAtPosition(tabPosition); + } + + /** + * Select a tab at the specified position. + * + * @param position the position to select. + */ + public void selectTabAtPosition(int position) { + selectTabAtPosition(position, false); + } + + /** + * Select a tab at the specified position. + * + * @param position the position to select. + * @param animate should the tab change be animated or not. + */ + public void selectTabAtPosition(int position, boolean animate) { + if (position > getTabCount() - 1 || position < 0) { + throw new IndexOutOfBoundsException("Can't select tab at position " + + position + ". This BottomBar has no items at that position."); + } + + BottomBarTab oldTab = getCurrentTab(); + BottomBarTab newTab = getTabAtPosition(position); + + oldTab.deselect(animate); + newTab.select(animate); + + updateSelectedTab(position); + shiftingMagic(oldTab, newTab, animate); + handleBackgroundColorChange(newTab, animate); + } + + public int getTabCount() { + return tabContainer.getChildCount(); + } + + /** + * Get the currently selected tab. + */ + public BottomBarTab getCurrentTab() { + return getTabAtPosition(getCurrentTabPosition()); + } + + /** + * Get the tab at the specified position. + */ + public BottomBarTab getTabAtPosition(int position) { + View child = tabContainer.getChildAt(position); + + if (child instanceof BadgeContainer) { + return findTabInLayout((BadgeContainer) child); + } + + return (BottomBarTab) child; + } + + /** + * Get the resource id for the currently selected tab. + */ + @IdRes + public int getCurrentTabId() { + return getCurrentTab().getId(); + } + + /** + * Get the currently selected tab position. + */ + public int getCurrentTabPosition() { + return currentTabPosition; + } + + /** + * Find the tabs' position in the container by id. + */ + public int findPositionForTabWithId(@IdRes int tabId) { + return getTabWithId(tabId).getIndexInTabContainer(); + } + + /** + * Find a BottomBarTab with the corresponding id. + */ + public BottomBarTab getTabWithId(@IdRes int tabId) { + return (BottomBarTab) tabContainer.findViewById(tabId); + } + + /** + * Controls whether the long pressed tab title should be displayed in + * a helpful Toast if the title is not currently visible. + * + * @param enabled true if toasts should be shown to indicate the title + * of a long pressed tab, false otherwise. + */ + public void setLongPressHintsEnabled(boolean enabled) { + longPressHintsEnabled = enabled; + } + + /** + * Set alpha value used for inactive BottomBarTabs. + */ + public void setInActiveTabAlpha(float alpha) { + inActiveTabAlpha = alpha; + + batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() { + @Override + public void update(BottomBarTab tab) { + tab.setInActiveAlpha(inActiveTabAlpha); + } + }); + } + + /** + * Set alpha value used for active BottomBarTabs. + */ + public void setActiveTabAlpha(float alpha) { + activeTabAlpha = alpha; + + batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() { + @Override + public void update(BottomBarTab tab) { + tab.setActiveAlpha(activeTabAlpha); + } + }); + } + + public void setInActiveTabColor(@ColorInt int color) { + inActiveTabColor = color; + + batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() { + @Override + public void update(BottomBarTab tab) { + tab.setInActiveColor(inActiveTabColor); + } + }); + } + + /** + * Set active color used for selected BottomBarTabs. + */ + public void setActiveTabColor(@ColorInt int color) { + activeTabColor = color; + + batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() { + @Override + public void update(BottomBarTab tab) { + tab.setActiveColor(activeTabColor); + } + }); + } + + /** + * Set background color for the badge. + */ + public void setBadgeBackgroundColor(@ColorInt int color) { + badgeBackgroundColor = color; + + batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() { + @Override + public void update(BottomBarTab tab) { + tab.setBadgeBackgroundColor(badgeBackgroundColor); + } + }); + } + + /** + * Controls whether the badge (if any) for active tabs + * should be hidden or not. + */ + public void setBadgesHideWhenActive(final boolean hideWhenSelected) { + hideBadgeWhenActive = hideWhenSelected; + batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() { + @Override + public void update(BottomBarTab tab) { + tab.setBadgeHidesWhenActive(hideWhenSelected); + } + }); + } + + /** + * Set custom text apperance for all BottomBarTabs. + */ + public void setTabTitleTextAppearance(int textAppearance) { + titleTextAppearance = textAppearance; + + batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() { + @Override + public void update(BottomBarTab tab) { + tab.setTitleTextAppearance(titleTextAppearance); + } + }); + } + + /** + * Set a custom typeface for all tab's titles. + * + * @param fontPath path for your custom font file, such as fonts/MySuperDuperFont.ttf. + * In that case your font path would look like src/main/assets/fonts/MySuperDuperFont.ttf, + * but you only need to provide fonts/MySuperDuperFont.ttf, as the asset folder + * will be auto-filled for you. + */ + public void setTabTitleTypeface(String fontPath) { + Typeface actualTypeface = getTypeFaceFromAsset(fontPath); + setTabTitleTypeface(actualTypeface); + } + + /** + * Set a custom typeface for all tab's titles. + */ + public void setTabTitleTypeface(Typeface typeface) { + titleTypeFace = typeface; + + batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() { + @Override + public void update(BottomBarTab tab) { + tab.setTitleTypeface(titleTypeFace); + } + }); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (changed) { + if (!isTabletMode) { + resizeTabsToCorrectSizes(currentTabs); + } + + updateTitleBottomPadding(); + + if (isShy()) { + initializeShyBehavior(); + } + + if (drawUnderNav()) { + resizeForDrawingUnderNavbar(); + } + } + } + + private void updateTitleBottomPadding() { + if (isIconsOnlyMode()) { + return; + } + + int tabCount = getTabCount(); + + if (tabContainer == null || tabCount == 0 || !isShiftingMode()) { + return; + } + + for (int i = 0; i < tabCount; i++) { + BottomBarTab tab = getTabAtPosition(i); + TextView title = tab.getTitleView(); + + if (title == null) { + continue; + } + + int baseline = title.getBaseline(); + int height = title.getHeight(); + int paddingInsideTitle = height - baseline; + int missingPadding = tenDp - paddingInsideTitle; + + if (missingPadding > 0) { + title.setPadding(title.getPaddingLeft(), title.getPaddingTop(), + title.getPaddingRight(), missingPadding + title.getPaddingBottom()); + } + } + } + + private void initializeShyBehavior() { + ViewParent parent = getParent(); + + boolean hasAbusiveParent = parent != null + && parent instanceof CoordinatorLayout; + + if (!hasAbusiveParent) { + throw new RuntimeException("In order to have shy behavior, the " + + "BottomBar must be a direct child of a CoordinatorLayout."); + } + + if (!shyHeightAlreadyCalculated) { + int height = getHeight(); + + if (height != 0) { + updateShyHeight(height); + getShySettings().shyHeightCalculated(); + shyHeightAlreadyCalculated = true; + } + } + } + + private void updateShyHeight(int height) { + ((CoordinatorLayout.LayoutParams) getLayoutParams()) + .setBehavior(new BottomNavigationBehavior(height, 0, false)); + } + + private void resizeForDrawingUnderNavbar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + int currentHeight = getHeight(); + + if (currentHeight != 0 && !navBarAccountedHeightCalculated) { + navBarAccountedHeightCalculated = true; + tabContainer.getLayoutParams().height = currentHeight; + + int navbarHeight = NavbarUtils.INSTANCE.getNavbarHeight(getContext()); + int finalHeight = currentHeight + navbarHeight; + getLayoutParams().height = finalHeight; + + if (isShy()) { + updateShyHeight(finalHeight); + } + } + } + } + + @Override + public Parcelable onSaveInstanceState() { + Bundle bundle = saveState(); + bundle.putParcelable("superstate", super.onSaveInstanceState()); + return bundle; + } + + @VisibleForTesting + Bundle saveState() { + Bundle outState = new Bundle(); + outState.putInt(STATE_CURRENT_SELECTED_TAB, currentTabPosition); + + return outState; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof Bundle) { + Bundle bundle = (Bundle) state; + restoreState(bundle); + + state = bundle.getParcelable("superstate"); + } + super.onRestoreInstanceState(state); + } + + @VisibleForTesting + void restoreState(Bundle savedInstanceState) { + if (savedInstanceState != null) { + isComingFromRestoredState = true; + ignoreTabReselectionListener = true; + + int restoredPosition = savedInstanceState.getInt(STATE_CURRENT_SELECTED_TAB, currentTabPosition); + selectTabAtPosition(restoredPosition, false); + } + } + + @Override + public void onClick(View target) { + if (!(target instanceof BottomBarTab)) return; + handleClick((BottomBarTab) target); + } + + @Override + public boolean onLongClick(View target) { + return !(target instanceof BottomBarTab) || handleLongClick((BottomBarTab) target); + } + + private BottomBarTab findTabInLayout(ViewGroup child) { + for (int i = 0; i < child.getChildCount(); i++) { + View candidate = child.getChildAt(i); + + if (candidate instanceof BottomBarTab) { + return (BottomBarTab) candidate; + } + } + + return null; + } + + private void handleClick(BottomBarTab newTab) { + BottomBarTab oldTab = getCurrentTab(); + + if (tabSelectionInterceptor != null + && tabSelectionInterceptor.shouldInterceptTabSelection(oldTab.getId(), newTab.getId())) { + return; + } + + oldTab.deselect(true); + newTab.select(true); + + shiftingMagic(oldTab, newTab, true); + handleBackgroundColorChange(newTab, true); + updateSelectedTab(newTab.getIndexInTabContainer()); + } + + private boolean handleLongClick(BottomBarTab longClickedTab) { + boolean areInactiveTitlesHidden = isShiftingMode() || isTabletMode; + boolean isClickedTitleHidden = !longClickedTab.isActive(); + boolean shouldShowHint = areInactiveTitlesHidden + && isClickedTitleHidden + && longPressHintsEnabled; + + if (shouldShowHint) { + Toast.makeText(getContext(), longClickedTab.getTitle(), Toast.LENGTH_SHORT) + .show(); + } + + return true; + } + + private void updateSelectedTab(int newPosition) { + int newTabId = getTabAtPosition(newPosition).getId(); + + if (newPosition != currentTabPosition) { + if (onTabSelectListener != null) { + onTabSelectListener.onTabSelected(newTabId); + } + } else if (onTabReselectListener != null && !ignoreTabReselectionListener) { + onTabReselectListener.onTabReSelected(newTabId); + } + + currentTabPosition = newPosition; + + if (ignoreTabReselectionListener) { + ignoreTabReselectionListener = false; + } + } + + private void shiftingMagic(BottomBarTab oldTab, BottomBarTab newTab, boolean animate) { + if (isShiftingMode()) { + oldTab.updateWidth(inActiveShiftingItemWidth, animate); + newTab.updateWidth(activeShiftingItemWidth, animate); + } + } + + private void handleBackgroundColorChange(BottomBarTab tab, boolean animate) { + int newColor = tab.getBarColorWhenSelected(); + + if (currentBackgroundColor == newColor) { + return; + } + + if (!animate) { + outerContainer.setBackgroundColor(newColor); + return; + } + + View clickedView = tab; + + if (tab.hasActiveBadge()) { + clickedView = tab.getOuterView(); + } + + animateBGColorChange(clickedView, newColor); + currentBackgroundColor = newColor; + } + + private void animateBGColorChange(View clickedView, final int newColor) { + prepareForBackgroundColorAnimation(newColor); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (!outerContainer.isAttachedToWindow()) { + return; + } + + backgroundCircularRevealAnimation(clickedView, newColor); + } else { + backgroundCrossfadeAnimation(newColor); + } + } + + private void prepareForBackgroundColorAnimation(int newColor) { + outerContainer.clearAnimation(); + backgroundOverlay.clearAnimation(); + + backgroundOverlay.setBackgroundColor(newColor); + backgroundOverlay.setVisibility(View.VISIBLE); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void backgroundCircularRevealAnimation(View clickedView, final int newColor) { + int centerX = (int) (ViewCompat.getX(clickedView) + (clickedView.getMeasuredWidth() / 2)); + int yOffset = isTabletMode ? (int) ViewCompat.getY(clickedView) : 0; + int centerY = yOffset + clickedView.getMeasuredHeight() / 2; + int startRadius = 0; + int finalRadius = isTabletMode ? outerContainer.getHeight() : outerContainer.getWidth(); + + Animator animator = ViewAnimationUtils.createCircularReveal( + backgroundOverlay, + centerX, + centerY, + startRadius, + finalRadius + ); + + if (isTabletMode) { + animator.setDuration(500); + } + + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + onEnd(); + } + + @Override + public void onAnimationCancel(Animator animation) { + onEnd(); + } + + private void onEnd() { + outerContainer.setBackgroundColor(newColor); + backgroundOverlay.setVisibility(View.INVISIBLE); + ViewCompat.setAlpha(backgroundOverlay, 1); + } + }); + + animator.start(); + } + + private void backgroundCrossfadeAnimation(final int newColor) { + ViewCompat.setAlpha(backgroundOverlay, 0); + ViewCompat.animate(backgroundOverlay) + .alpha(1) + .setListener(new ViewPropertyAnimatorListenerAdapter() { + @Override + public void onAnimationEnd(View view) { + onEnd(); + } + + @Override + public void onAnimationCancel(View view) { + onEnd(); + } + + private void onEnd() { + outerContainer.setBackgroundColor(newColor); + backgroundOverlay.setVisibility(View.INVISIBLE); + ViewCompat.setAlpha(backgroundOverlay, 1); + } + }) + .start(); + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarBadge.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarBadge.java new file mode 100644 index 000000000..d7aa57294 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarBadge.java @@ -0,0 +1,172 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.os.Build; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.TextView; + +import com.habitrpg.android.habitica.R; + +import androidx.appcompat.widget.AppCompatImageView; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.core.view.ViewCompat; + +/* + * BottomBar library for Android + * Copyright (c) 2016 Iiro Krankka (http://github.com/roughike). + * + * 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. + */ +class BottomBarBadge extends AppCompatTextView { + private int count; + private boolean isVisible = false; + + BottomBarBadge(Context context) { + super(context); + } + + /** + * Set the unread / new item / whatever count for this Badge. + * + * @param count the value this Badge should show. + */ + void setCount(int count) { + this.count = count; + setText(String.valueOf(count)); + } + + /** + * Get the currently showing count for this Badge. + * + * @return current count for the Badge. + */ + int getCount() { + return count; + } + + /** + * Shows the badge with a neat little scale animation. + */ + void show() { + isVisible = true; + ViewCompat.animate(this) + .setDuration(150) + .alpha(1) + .scaleX(1) + .scaleY(1) + .start(); + } + + /** + * Hides the badge with a neat little scale animation. + */ + void hide() { + isVisible = false; + ViewCompat.animate(this) + .setDuration(150) + .alpha(0) + .scaleX(0) + .scaleY(0) + .start(); + } + + /** + * Is this badge currently visible? + * + * @return true is this badge is visible, otherwise false. + */ + boolean isVisible() { + return isVisible; + } + + void attachToTab(BottomBarTab tab, int backgroundColor) { + ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + setLayoutParams(params); + setGravity(Gravity.CENTER); + MiscUtils.setTextAppearance(this, R.style.BB_BottomBarBadge_Text); + + setColoredCircleBackground(backgroundColor); + wrapTabAndBadgeInSameContainer(tab); + } + + void setColoredCircleBackground(int circleColor) { + int innerPadding = MiscUtils.dpToPixel(getContext(), 1); + ShapeDrawable backgroundCircle = BadgeCircle.make(innerPadding * 3, circleColor); + setPadding(innerPadding, innerPadding, innerPadding, innerPadding); + setBackgroundCompat(backgroundCircle); + } + + private void wrapTabAndBadgeInSameContainer(final BottomBarTab tab) { + ViewGroup tabContainer = (ViewGroup) tab.getParent(); + tabContainer.removeView(tab); + + final BadgeContainer badgeContainer = new BadgeContainer(getContext()); + badgeContainer.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + badgeContainer.addView(tab); + badgeContainer.addView(this); + + tabContainer.addView(badgeContainer, tab.getIndexInTabContainer()); + + badgeContainer.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @SuppressWarnings("deprecation") + @Override + public void onGlobalLayout() { + badgeContainer.getViewTreeObserver().removeGlobalOnLayoutListener(this); + adjustPositionAndSize(tab); + } + }); + } + + void removeFromTab(BottomBarTab tab) { + BadgeContainer badgeAndTabContainer = (BadgeContainer) getParent(); + ViewGroup originalTabContainer = (ViewGroup) badgeAndTabContainer.getParent(); + + badgeAndTabContainer.removeView(tab); + originalTabContainer.removeView(badgeAndTabContainer); + originalTabContainer.addView(tab, tab.getIndexInTabContainer()); + } + + void adjustPositionAndSize(BottomBarTab tab) { + AppCompatImageView iconView = tab.getIconView(); + ViewGroup.LayoutParams params = getLayoutParams(); + + int size = Math.max(getWidth(), getHeight()); + float xOffset = (float) (iconView.getWidth() / 1.25); + + setX(iconView.getX() + xOffset); + setTranslationY(10); + + if (params.width != size || params.height != size) { + params.width = size; + params.height = size; + setLayoutParams(params); + } + } + + @SuppressWarnings("deprecation") + private void setBackgroundCompat(Drawable background) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + setBackground(background); + } else { + setBackgroundDrawable(background); + } + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarTab.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarTab.java new file mode 100644 index 000000000..d3f8d73f4 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarTab.java @@ -0,0 +1,730 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.core.view.ViewCompat; +import androidx.core.view.ViewPropertyAnimatorCompat; + +import android.view.Gravity; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.habitrpg.android.habitica.R; + +/* + * BottomBar library for Android + * Copyright (c) 2016 Iiro Krankka (http://github.com/roughike). + * + * 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. + */ +public class BottomBarTab extends LinearLayout { + @VisibleForTesting + static final String STATE_BADGE_COUNT = "STATE_BADGE_COUNT_FOR_TAB_"; + + private static final long ANIMATION_DURATION = 150; + private static final float ACTIVE_TITLE_SCALE = 1; + private static final float INACTIVE_FIXED_TITLE_SCALE = 0.86f; + private static final float ACTIVE_SHIFTING_TITLELESS_ICON_SCALE = 1.24f; + private static final float INACTIVE_SHIFTING_TITLELESS_ICON_SCALE = 1f; + + private final int sixDps; + private final int eightDps; + private final int sixteenDps; + + @VisibleForTesting + BottomBarBadge badge; + + private Type type = Type.FIXED; + private boolean isTitleless; + private int iconResId; + private String title; + private float inActiveAlpha; + private float activeAlpha; + private int inActiveColor; + private int activeColor; + private int barColorWhenSelected; + private int badgeBackgroundColor; + private boolean badgeHidesWhenActive; + private AppCompatImageView iconView; + private TextView titleView; + private boolean isActive; + private int indexInContainer; + private int titleTextAppearanceResId; + private Typeface titleTypeFace; + + BottomBarTab(Context context) { + super(context); + + sixDps = MiscUtils.dpToPixel(context, 6); + eightDps = MiscUtils.dpToPixel(context, 8); + sixteenDps = MiscUtils.dpToPixel(context, 16); + } + + void setConfig(@NonNull Config config) { + setInActiveAlpha(config.inActiveTabAlpha); + setActiveAlpha(config.activeTabAlpha); + setInActiveColor(config.inActiveTabColor); + setActiveColor(config.activeTabColor); + setBarColorWhenSelected(config.barColorWhenSelected); + setBadgeBackgroundColor(config.badgeBackgroundColor); + setBadgeHidesWhenActive(config.badgeHidesWhenSelected); + setTitleTextAppearance(config.titleTextAppearance); + setTitleTypeface(config.titleTypeFace); + } + + void prepareLayout() { + inflate(getContext(), getLayoutResource(), this); + setOrientation(VERTICAL); + setGravity(isTitleless? Gravity.CENTER : Gravity.CENTER_HORIZONTAL); + setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + setBackgroundResource(MiscUtils.getDrawableRes(getContext(), R.attr.selectableItemBackgroundBorderless)); + + iconView = (AppCompatImageView) findViewById(R.id.bb_bottom_bar_icon); + iconView.setImageResource(iconResId); + + if (type != Type.TABLET && !isTitleless) { + titleView = (TextView) findViewById(R.id.bb_bottom_bar_title); + titleView.setVisibility(VISIBLE); + + if (type == Type.SHIFTING) { + findViewById(R.id.spacer).setVisibility(VISIBLE); + } + + updateTitle(); + } + + updateCustomTextAppearance(); + updateCustomTypeface(); + } + + @VisibleForTesting + int getLayoutResource() { + int layoutResource; + switch (type) { + case FIXED: + layoutResource = R.layout.bb_bottom_bar_item_fixed; + break; + case SHIFTING: + layoutResource = R.layout.bb_bottom_bar_item_shifting; + break; + case TABLET: + layoutResource = R.layout.bb_bottom_bar_item_fixed_tablet; + break; + default: + // should never happen + throw new RuntimeException("Unknown BottomBarTab type."); + } + return layoutResource; + } + + private void updateTitle() { + if (titleView != null) { + titleView.setText(title); + } + } + + @SuppressWarnings("deprecation") + private void updateCustomTextAppearance() { + if (titleView == null || titleTextAppearanceResId == 0) { + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + titleView.setTextAppearance(titleTextAppearanceResId); + } else { + titleView.setTextAppearance(getContext(), titleTextAppearanceResId); + } + + titleView.setTag(R.id.bb_bottom_bar_appearance_id, titleTextAppearanceResId); + } + + private void updateCustomTypeface() { + if (titleTypeFace != null && titleView != null) { + titleView.setTypeface(titleTypeFace); + } + } + + Type getType() { + return type; + } + + void setType(Type type) { + this.type = type; + } + + boolean isTitleless() { + return isTitleless; + } + + void setIsTitleless(boolean isTitleless) { + if (isTitleless && getIconResId() == 0) { + throw new IllegalStateException("This tab is supposed to be " + + "icon only, yet it has no icon specified. Index in " + + "container: " + getIndexInTabContainer()); + } + + this.isTitleless = isTitleless; + } + + public ViewGroup getOuterView() { + return (ViewGroup) getParent(); + } + + AppCompatImageView getIconView() { + return iconView; + } + + int getIconResId() { + return iconResId; + } + + void setIconResId(int iconResId) { + this.iconResId = iconResId; + } + + TextView getTitleView() { + return titleView; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + updateTitle(); + } + + public float getInActiveAlpha() { + return inActiveAlpha; + } + + public void setInActiveAlpha(float inActiveAlpha) { + this.inActiveAlpha = inActiveAlpha; + + if (!isActive) { + setAlphas(inActiveAlpha); + } + } + + public float getActiveAlpha() { + return activeAlpha; + } + + public void setActiveAlpha(float activeAlpha) { + this.activeAlpha = activeAlpha; + + if (isActive) { + setAlphas(activeAlpha); + } + } + + public int getInActiveColor() { + return inActiveColor; + } + + public void setInActiveColor(int inActiveColor) { + this.inActiveColor = inActiveColor; + + if (!isActive) { + setColors(inActiveColor); + } + } + + public int getActiveColor() { + return activeColor; + } + + public void setActiveColor(int activeIconColor) { + this.activeColor = activeIconColor; + + if (isActive) { + setColors(activeColor); + } + } + + public int getBarColorWhenSelected() { + return barColorWhenSelected; + } + + public void setBarColorWhenSelected(int barColorWhenSelected) { + this.barColorWhenSelected = barColorWhenSelected; + } + + public int getBadgeBackgroundColor() { + return badgeBackgroundColor; + } + + public void setBadgeBackgroundColor(int badgeBackgroundColor) { + this.badgeBackgroundColor = badgeBackgroundColor; + + if (badge != null) { + badge.setColoredCircleBackground(badgeBackgroundColor); + } + } + + public boolean getBadgeHidesWhenActive() { + return badgeHidesWhenActive; + } + + public void setBadgeHidesWhenActive(boolean hideWhenActive) { + this.badgeHidesWhenActive = hideWhenActive; + } + + int getCurrentDisplayedIconColor() { + Object tag = iconView.getTag(R.id.bb_bottom_bar_color_id); + + if (tag instanceof Integer) { + return (int) tag; + } + + return 0; + } + + int getCurrentDisplayedTitleColor() { + if (titleView != null) { + return titleView.getCurrentTextColor(); + } + + return 0; + } + + int getCurrentDisplayedTextAppearance() { + Object tag = titleView.getTag(R.id.bb_bottom_bar_appearance_id); + + if (titleView != null && tag instanceof Integer) { + return (int) tag; + } + + return 0; + } + + public void setBadgeCount(int count) { + if (count <= 0) { + if (badge != null) { + badge.removeFromTab(this); + badge = null; + } + + return; + } + + if (badge == null) { + badge = new BottomBarBadge(getContext()); + badge.attachToTab(this, badgeBackgroundColor); + } + + badge.setCount(count); + + if (isActive && badgeHidesWhenActive) { + badge.hide(); + } + } + + public void removeBadge() { + setBadgeCount(0); + } + + boolean isActive() { + return isActive; + } + + boolean hasActiveBadge() { + return badge != null; + } + + int getIndexInTabContainer() { + return indexInContainer; + } + + void setIndexInContainer(int indexInContainer) { + this.indexInContainer = indexInContainer; + } + + void setIconTint(int tint) { + iconView.setColorFilter(tint); + } + + public int getTitleTextAppearance() { + return titleTextAppearanceResId; + } + + @SuppressWarnings("deprecation") + void setTitleTextAppearance(int resId) { + this.titleTextAppearanceResId = resId; + updateCustomTextAppearance(); + } + + public void setTitleTypeface(Typeface typeface) { + this.titleTypeFace = typeface; + updateCustomTypeface(); + } + + public Typeface getTitleTypeFace() { + return titleTypeFace; + } + + void select(boolean animate) { + isActive = true; + + if (animate) { + animateIcon(activeAlpha, ACTIVE_SHIFTING_TITLELESS_ICON_SCALE); + animateTitle(sixDps, ACTIVE_TITLE_SCALE, activeAlpha); + animateColors(inActiveColor, activeColor); + } else { + setTitleScale(ACTIVE_TITLE_SCALE); + setTopPadding(sixDps); + setIconScale(ACTIVE_SHIFTING_TITLELESS_ICON_SCALE); + setColors(activeColor); + setAlphas(activeAlpha); + } + + setSelected(true); + + if (badge != null && badgeHidesWhenActive) { + badge.hide(); + } + } + + void deselect(boolean animate) { + isActive = false; + + boolean isShifting = type == Type.SHIFTING; + + float titleScale = isShifting ? 0 : INACTIVE_FIXED_TITLE_SCALE; + int iconPaddingTop = isShifting ? sixteenDps : eightDps; + + if (animate) { + animateTitle(iconPaddingTop, titleScale, inActiveAlpha); + animateIcon(inActiveAlpha, INACTIVE_SHIFTING_TITLELESS_ICON_SCALE); + animateColors(activeColor, inActiveColor); + } else { + setTitleScale(titleScale); + setTopPadding(iconPaddingTop); + setIconScale(INACTIVE_SHIFTING_TITLELESS_ICON_SCALE); + setColors(inActiveColor); + setAlphas(inActiveAlpha); + } + + setSelected(false); + + if (!isShifting && badge != null && !badge.isVisible()) { + badge.show(); + } + } + + private void animateColors(int previousColor, int color) { + ValueAnimator anim = new ValueAnimator(); + anim.setIntValues(previousColor, color); + anim.setEvaluator(new ArgbEvaluator()); + anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + setColors((Integer) valueAnimator.getAnimatedValue()); + } + }); + + anim.setDuration(150); + anim.start(); + } + + private void setColors(int color) { + if (iconView != null) { + iconView.setColorFilter(color); + iconView.setTag(R.id.bb_bottom_bar_color_id, color); + } + + if (titleView != null) { + titleView.setTextColor(color); + } + } + + private void setAlphas(float alpha) { + if (iconView != null) { + ViewCompat.setAlpha(iconView, alpha); + } + + if (titleView != null) { + ViewCompat.setAlpha(titleView, alpha); + } + } + + void updateWidth(float endWidth, boolean animated) { + if (!animated) { + getLayoutParams().width = (int) endWidth; + + if (!isActive && badge != null) { + badge.adjustPositionAndSize(this); + badge.show(); + } + return; + } + + float start = getWidth(); + + ValueAnimator animator = ValueAnimator.ofFloat(start, endWidth); + animator.setDuration(150); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + ViewGroup.LayoutParams params = getLayoutParams(); + if (params == null) return; + + params.width = Math.round((float) animator.getAnimatedValue()); + setLayoutParams(params); + } + }); + + // Workaround to avoid using faulty onAnimationEnd() listener + postDelayed(new Runnable() { + @Override + public void run() { + if (!isActive && badge != null) { + clearAnimation(); + badge.adjustPositionAndSize(BottomBarTab.this); + badge.show(); + } + } + }, animator.getDuration()); + + animator.start(); + } + + private void updateBadgePosition() { + if (badge != null) { + badge.adjustPositionAndSize(this); + } + } + + private void setTopPaddingAnimated(int start, int end) { + if (type == Type.TABLET || isTitleless) { + return; + } + + ValueAnimator paddingAnimator = ValueAnimator.ofInt(start, end); + paddingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + iconView.setPadding( + iconView.getPaddingLeft(), + (Integer) animation.getAnimatedValue(), + iconView.getPaddingRight(), + iconView.getPaddingBottom() + ); + } + }); + + paddingAnimator.setDuration(ANIMATION_DURATION); + paddingAnimator.start(); + } + + private void animateTitle(int padding, float scale, float alpha) { + if (type == Type.TABLET && isTitleless) { + return; + } + + setTopPaddingAnimated(iconView.getPaddingTop(), padding); + + ViewPropertyAnimatorCompat titleAnimator = ViewCompat.animate(titleView) + .setDuration(ANIMATION_DURATION) + .scaleX(scale) + .scaleY(scale); + titleAnimator.alpha(alpha); + titleAnimator.start(); + } + + private void animateIconScale(float scale) { + ViewCompat.animate(iconView) + .setDuration(ANIMATION_DURATION) + .scaleX(scale) + .scaleY(scale) + .start(); + } + + private void animateIcon(float alpha, float scale) { + ViewCompat.animate(iconView) + .setDuration(ANIMATION_DURATION) + .alpha(alpha) + .start(); + + if (isTitleless && type == Type.SHIFTING) { + animateIconScale(scale); + } + } + + private void setTopPadding(int topPadding) { + if (type == Type.TABLET || isTitleless) { + return; + } + + iconView.setPadding( + iconView.getPaddingLeft(), + topPadding, + iconView.getPaddingRight(), + iconView.getPaddingBottom() + ); + } + + private void setTitleScale(float scale) { + if (type == Type.TABLET || isTitleless) { + return; + } + + ViewCompat.setScaleX(titleView, scale); + ViewCompat.setScaleY(titleView, scale); + } + + private void setIconScale(float scale) { + if (isTitleless && type == Type.SHIFTING) { + ViewCompat.setScaleX(iconView, scale); + ViewCompat.setScaleY(iconView, scale); + } + } + + @Override + public Parcelable onSaveInstanceState() { + if (badge != null) { + Bundle bundle = saveState(); + bundle.putParcelable("superstate", super.onSaveInstanceState()); + + return bundle; + } + + return super.onSaveInstanceState(); + } + + @VisibleForTesting + Bundle saveState() { + Bundle outState = new Bundle(); + outState.putInt(STATE_BADGE_COUNT + getIndexInTabContainer(), badge.getCount()); + + return outState; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof Bundle) { + Bundle bundle = (Bundle) state; + restoreState(bundle); + + state = bundle.getParcelable("superstate"); + } + + super.onRestoreInstanceState(state); + } + + @VisibleForTesting + void restoreState(Bundle savedInstanceState) { + int previousBadgeCount = savedInstanceState.getInt(STATE_BADGE_COUNT + getIndexInTabContainer()); + setBadgeCount(previousBadgeCount); + } + + enum Type { + FIXED, SHIFTING, TABLET + } + + public static class Config { + private final float inActiveTabAlpha; + private final float activeTabAlpha; + private final int inActiveTabColor; + private final int activeTabColor; + private final int barColorWhenSelected; + private final int badgeBackgroundColor; + private final int titleTextAppearance; + private final Typeface titleTypeFace; + private boolean badgeHidesWhenSelected = true; + + private Config(Builder builder) { + this.inActiveTabAlpha = builder.inActiveTabAlpha; + this.activeTabAlpha = builder.activeTabAlpha; + this.inActiveTabColor = builder.inActiveTabColor; + this.activeTabColor = builder.activeTabColor; + this.barColorWhenSelected = builder.barColorWhenSelected; + this.badgeBackgroundColor = builder.badgeBackgroundColor; + this.badgeHidesWhenSelected = builder.hidesBadgeWhenSelected; + this.titleTextAppearance = builder.titleTextAppearance; + this.titleTypeFace = builder.titleTypeFace; + } + + public static class Builder { + private float inActiveTabAlpha; + private float activeTabAlpha; + private int inActiveTabColor; + private int activeTabColor; + private int barColorWhenSelected; + private int badgeBackgroundColor; + private boolean hidesBadgeWhenSelected = true; + private int titleTextAppearance; + private Typeface titleTypeFace; + + public Builder inActiveTabAlpha(float alpha) { + this.inActiveTabAlpha = alpha; + return this; + } + + public Builder activeTabAlpha(float alpha) { + this.activeTabAlpha = alpha; + return this; + } + + public Builder inActiveTabColor(@ColorInt int color) { + this.inActiveTabColor = color; + return this; + } + + public Builder activeTabColor(@ColorInt int color) { + this.activeTabColor = color; + return this; + } + + public Builder barColorWhenSelected(@ColorInt int color) { + this.barColorWhenSelected = color; + return this; + } + + public Builder badgeBackgroundColor(@ColorInt int color) { + this.badgeBackgroundColor = color; + return this; + } + + public Builder hideBadgeWhenSelected(boolean hide) { + this.hidesBadgeWhenSelected = hide; + return this; + } + + public Builder titleTextAppearance(int titleTextAppearance) { + this.titleTextAppearance = titleTextAppearance; + return this; + } + + public Builder titleTypeFace(Typeface titleTypeFace) { + this.titleTypeFace = titleTypeFace; + return this; + } + + public Config build() { + return new Config(this); + } + } + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomNavigationBehavior.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomNavigationBehavior.java new file mode 100644 index 000000000..536139bac --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomNavigationBehavior.java @@ -0,0 +1,177 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.ViewCompat; +import androidx.core.view.ViewPropertyAnimatorCompat; +import androidx.interpolator.view.animation.LinearOutSlowInInterpolator; + +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; + +import com.google.android.material.snackbar.Snackbar; + +/** + * Created by Nikola D. on 3/15/2016. + * + * Credit goes to Nikola Despotoski: + * https://github.com/NikolaDespotoski + */ +class BottomNavigationBehavior extends VerticalScrollingBehavior { + private static final Interpolator INTERPOLATOR = new LinearOutSlowInInterpolator(); + private final int bottomNavHeight; + private final int defaultOffset; + private boolean isTablet = false; + + private ViewPropertyAnimatorCompat mTranslationAnimator; + private boolean hidden = false; + private int mSnackbarHeight = -1; + private final BottomNavigationWithSnackbar mWithSnackBarImpl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? new LollipopBottomNavWithSnackBarImpl() : new PreLollipopBottomNavWithSnackBarImpl(); + private boolean mScrollingEnabled = true; + + BottomNavigationBehavior(int bottomNavHeight, int defaultOffset, boolean tablet) { + this.bottomNavHeight = bottomNavHeight; + this.defaultOffset = defaultOffset; + isTablet = tablet; + } + + @Override + public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { + mWithSnackBarImpl.updateSnackbar(parent, dependency, child); + return dependency instanceof Snackbar.SnackbarLayout; + } + + @Override + public void onNestedVerticalOverScroll(CoordinatorLayout coordinatorLayout, V child, @ScrollDirection int direction, int currentOverScroll, int totalOverScroll) { + } + + @Override + public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) { + updateScrollingForSnackbar(dependency, true); + super.onDependentViewRemoved(parent, child, dependency); + } + + private void updateScrollingForSnackbar(View dependency, boolean enabled) { + if (!isTablet && dependency instanceof Snackbar.SnackbarLayout) { + mScrollingEnabled = enabled; + } + } + + @Override + public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) { + updateScrollingForSnackbar(dependency, false); + return super.onDependentViewChanged(parent, child, dependency); + } + + @Override + public void onDirectionNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed, @ScrollDirection int scrollDirection) { + handleDirection(child, scrollDirection); + } + + private void handleDirection(V child, int scrollDirection) { + if (!mScrollingEnabled) return; + if (scrollDirection == ScrollDirection.SCROLL_DIRECTION_DOWN && hidden) { + hidden = false; + animateOffset(child, defaultOffset); + } else if (scrollDirection == ScrollDirection.SCROLL_DIRECTION_UP && !hidden) { + hidden = true; + animateOffset(child, bottomNavHeight + defaultOffset); + } + } + + @Override + protected boolean onNestedDirectionFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY, @ScrollDirection int scrollDirection) { + handleDirection(child, scrollDirection); + return true; + } + + private void animateOffset(final V child, final int offset) { + ensureOrCancelAnimator(child); + mTranslationAnimator.translationY(offset).start(); + } + + private void ensureOrCancelAnimator(V child) { + if (mTranslationAnimator == null) { + mTranslationAnimator = ViewCompat.animate(child); + mTranslationAnimator.setDuration(300); + mTranslationAnimator.setInterpolator(INTERPOLATOR); + } else { + mTranslationAnimator.cancel(); + } + } + + + void setHidden(@NonNull V view, boolean bottomLayoutHidden) { + if (!bottomLayoutHidden && hidden) { + animateOffset(view, defaultOffset); + } else if (bottomLayoutHidden && !hidden) { + animateOffset(view, bottomNavHeight + defaultOffset); + } + hidden = bottomLayoutHidden; + } + + + static BottomNavigationBehavior from(@NonNull V view) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + + if (!(params instanceof CoordinatorLayout.LayoutParams)) { + throw new IllegalArgumentException("The view is not a child of CoordinatorLayout"); + } + + CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params) + .getBehavior(); + + if (behavior instanceof BottomNavigationBehavior) { + // noinspection unchecked + return (BottomNavigationBehavior) behavior; + } + + throw new IllegalArgumentException("The view is not associated with BottomNavigationBehavior"); + } + + private interface BottomNavigationWithSnackbar { + void updateSnackbar(CoordinatorLayout parent, View dependency, View child); + } + + + private class PreLollipopBottomNavWithSnackBarImpl implements BottomNavigationWithSnackbar { + + @Override + public void updateSnackbar(CoordinatorLayout parent, View dependency, View child) { + if (!isTablet && dependency instanceof Snackbar.SnackbarLayout) { + if (mSnackbarHeight == -1) { + mSnackbarHeight = dependency.getHeight(); + } + if (ViewCompat.getTranslationY(child) != 0) return; + int targetPadding = bottomNavHeight + mSnackbarHeight - defaultOffset; + + ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) dependency.getLayoutParams(); + layoutParams.bottomMargin = targetPadding; + child.bringToFront(); + child.getParent().requestLayout(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + ((View) child.getParent()).invalidate(); + } + + } + } + } + + private class LollipopBottomNavWithSnackBarImpl implements BottomNavigationWithSnackbar { + @Override + public void updateSnackbar(CoordinatorLayout parent, View dependency, View child) { + if (!isTablet && dependency instanceof Snackbar.SnackbarLayout) { + if (mSnackbarHeight == -1) { + mSnackbarHeight = dependency.getHeight(); + } + if (ViewCompat.getTranslationY(child) != 0) return; + int targetPadding = (mSnackbarHeight + bottomNavHeight - defaultOffset); + dependency.setPadding(dependency.getPaddingLeft(), + dependency.getPaddingTop(), dependency.getPaddingRight(), targetPadding + ); + } + } + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/MiscUtils.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/MiscUtils.java new file mode 100644 index 000000000..cdc743907 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/MiscUtils.java @@ -0,0 +1,121 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import androidx.annotation.AttrRes; +import androidx.annotation.ColorInt; +import androidx.annotation.Dimension; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Px; +import androidx.annotation.StyleRes; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.widget.TextView; + +import static androidx.annotation.Dimension.DP; + +/* + * BottomBar library for Android + * Copyright (c) 2016 Iiro Krankka (http://github.com/roughike). + * + * 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. + */ +class MiscUtils { + + @NonNull protected static TypedValue getTypedValue(@NonNull Context context, @AttrRes int resId) { + TypedValue tv = new TypedValue(); + context.getTheme().resolveAttribute(resId, tv, true); + return tv; + } + + @ColorInt + protected static int getColor(@NonNull Context context, @AttrRes int color) { + return getTypedValue(context, color).data; + } + + @DrawableRes + protected static int getDrawableRes(@NonNull Context context, @AttrRes int drawable) { + return getTypedValue(context, drawable).resourceId; + } + + /** + * Converts dps to pixels nicely. + * + * @param context the Context for getting the resources + * @param dp dimension in dps + * @return dimension in pixels + */ + protected static int dpToPixel(@NonNull Context context, @Dimension(unit = DP) float dp) { + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + + try { + return (int) (dp * metrics.density); + } catch (NoSuchFieldError ignored) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics); + } + } + + /** + * Converts pixels to dps just as well. + * + * @param context the Context for getting the resources + * @param px dimension in pixels + * @return dimension in dps + */ + protected static int pixelToDp(@NonNull Context context, @Px int px) { + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + return Math.round(px / displayMetrics.density); + } + + /** + * Returns screen width. + * + * @param context Context to get resources and device specific display metrics + * @return screen width + */ + protected static int getScreenWidth(@NonNull Context context) { + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + return (int) (displayMetrics.widthPixels / displayMetrics.density); + } + + /** + * A convenience method for setting text appearance. + * + * @param textView a TextView which textAppearance to modify. + * @param resId a style resource for the text appearance. + */ + @SuppressWarnings("deprecation") + protected static void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + textView.setTextAppearance(resId); + } else { + textView.setTextAppearance(textView.getContext(), resId); + } + } + + /** + * Determine if the current UI Mode is Night Mode. + * + * @param context Context to get the configuration. + * @return true if the night mode is enabled, otherwise false. + */ + protected static boolean isNightMode(@NonNull Context context) { + int currentNightMode = context.getResources().getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK; + return currentNightMode == Configuration.UI_MODE_NIGHT_YES; + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/OnTabReselectListener.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/OnTabReselectListener.java new file mode 100644 index 000000000..e8e5febd6 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/OnTabReselectListener.java @@ -0,0 +1,30 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +import androidx.annotation.IdRes; + +/* + * BottomBar library for Android + * Copyright (c) 2016 Iiro Krankka (http://github.com/roughike). + * + * 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. + */ +public interface OnTabReselectListener { + /** + * The method being called when currently visible {@link BottomBarTab} is + * reselected. Use this method for scrolling to the top of your content, + * as recommended by the Material Design spec + * + * @param tabId the {@link BottomBarTab} that was reselected. + */ + void onTabReSelected(@IdRes int tabId); +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/OnTabSelectListener.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/OnTabSelectListener.java new file mode 100644 index 000000000..5d7f5374f --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/OnTabSelectListener.java @@ -0,0 +1,32 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +import androidx.annotation.IdRes; + +/* + * BottomBar library for Android + * Copyright (c) 2016 Iiro Krankka (http://github.com/roughike). + * + * 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. + */ +public interface OnTabSelectListener { + /** + * The method being called when currently visible {@link BottomBarTab} changes. + * + * This listener is fired for the first time after the items have been set and + * also after a configuration change, such as when screen orientation changes + * from portrait to landscape. + * + * @param tabId the new visible {@link BottomBarTab} + */ + void onTabSelected(@IdRes int tabId); +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/ShySettings.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/ShySettings.java new file mode 100644 index 000000000..02e3e4fe1 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/ShySettings.java @@ -0,0 +1,55 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +/** + * Settings specific for a shy BottomBar. + */ +public class ShySettings { + private BottomBar bottomBar; + private Boolean pendingIsVisibleInShyMode; + + ShySettings(BottomBar bottomBar) { + this.bottomBar = bottomBar; + } + + void shyHeightCalculated() { + updatePendingShyVisibility(); + } + + /** + * Shows the BottomBar if it was hidden, with a translate animation. + */ + public void showBar() { + toggleIsVisibleInShyMode(true); + } + + /** + * Hides the BottomBar in if it was visible, with a translate animation. + */ + public void hideBar() { + toggleIsVisibleInShyMode(false); + } + + private void toggleIsVisibleInShyMode(boolean visible) { + if (!bottomBar.isShy()) { + return; + } + + if (bottomBar.isShyHeightAlreadyCalculated()) { + BottomNavigationBehavior behavior = BottomNavigationBehavior.from(bottomBar); + + if (behavior != null) { + boolean isHidden = !visible; + behavior.setHidden(bottomBar, isHidden); + } + } else { + pendingIsVisibleInShyMode = true; + } + } + + private void updatePendingShyVisibility() { + if (pendingIsVisibleInShyMode != null) { + toggleIsVisibleInShyMode(pendingIsVisibleInShyMode); + pendingIsVisibleInShyMode = null; + } + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/TabParser.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/TabParser.java new file mode 100644 index 000000000..e4a6c9b26 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/TabParser.java @@ -0,0 +1,210 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +import android.content.Context; +import android.content.res.XmlResourceParser; +import android.graphics.Color; +import androidx.annotation.CheckResult; +import androidx.annotation.ColorInt; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringDef; +import androidx.annotation.XmlRes; +import androidx.core.content.ContextCompat; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +import static com.habitrpg.android.habitica.ui.views.bottombar.TabParser.TabAttribute.ACTIVE_COLOR; +import static com.habitrpg.android.habitica.ui.views.bottombar.TabParser.TabAttribute.BADGE_BACKGROUND_COLOR; +import static com.habitrpg.android.habitica.ui.views.bottombar.TabParser.TabAttribute.BADGE_HIDES_WHEN_ACTIVE; +import static com.habitrpg.android.habitica.ui.views.bottombar.TabParser.TabAttribute.BAR_COLOR_WHEN_SELECTED; +import static com.habitrpg.android.habitica.ui.views.bottombar.TabParser.TabAttribute.ICON; +import static com.habitrpg.android.habitica.ui.views.bottombar.TabParser.TabAttribute.ID; +import static com.habitrpg.android.habitica.ui.views.bottombar.TabParser.TabAttribute.INACTIVE_COLOR; +import static com.habitrpg.android.habitica.ui.views.bottombar.TabParser.TabAttribute.IS_TITLELESS; +import static com.habitrpg.android.habitica.ui.views.bottombar.TabParser.TabAttribute.TITLE; + +/** + * Created by iiro on 21.7.2016. + * + * BottomBar library for Android + * Copyright (c) 2016 Iiro Krankka (http://github.com/roughike). + * + * 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. + */ +class TabParser { + private static final String TAB_TAG = "tab"; + private static final int AVG_NUMBER_OF_TABS = 5; + private static final int COLOR_NOT_SET = -1; + private static final int RESOURCE_NOT_FOUND = 0; + + @NonNull + private final Context context; + + @NonNull + private final BottomBarTab.Config defaultTabConfig; + + @NonNull + private final XmlResourceParser parser; + + @Nullable + private List tabs = null; + + TabParser(@NonNull Context context, @NonNull BottomBarTab.Config defaultTabConfig, @XmlRes int tabsXmlResId) { + this.context = context; + this.defaultTabConfig = defaultTabConfig; + this.parser = context.getResources().getXml(tabsXmlResId); + } + + @CheckResult + @NonNull + public List parseTabs() { + if (tabs == null) { + tabs = new ArrayList<>(AVG_NUMBER_OF_TABS); + try { + int eventType; + do { + eventType = parser.next(); + if (eventType == XmlResourceParser.START_TAG && TAB_TAG.equals(parser.getName())) { + BottomBarTab bottomBarTab = parseNewTab(parser, tabs.size()); + tabs.add(bottomBarTab); + } + } while (eventType != XmlResourceParser.END_DOCUMENT); + } catch (IOException | XmlPullParserException e) { + e.printStackTrace(); + throw new TabParserException(); + } + } + + return tabs; + } + + @NonNull + private BottomBarTab parseNewTab(@NonNull XmlResourceParser parser, @IntRange(from = 0) int containerPosition) { + BottomBarTab workingTab = tabWithDefaults(); + workingTab.setIndexInContainer(containerPosition); + + final int numberOfAttributes = parser.getAttributeCount(); + for (int i = 0; i < numberOfAttributes; i++) { + @TabAttribute + String attrName = parser.getAttributeName(i); + switch (attrName) { + case ID: + workingTab.setId(parser.getIdAttributeResourceValue(i)); + break; + case ICON: + workingTab.setIconResId(parser.getAttributeResourceValue(i, RESOURCE_NOT_FOUND)); + break; + case TITLE: + workingTab.setTitle(getTitleValue(parser, i)); + break; + case INACTIVE_COLOR: + int inactiveColor = getColorValue(parser, i); + if (inactiveColor == COLOR_NOT_SET) continue; + workingTab.setInActiveColor(inactiveColor); + break; + case ACTIVE_COLOR: + int activeColor = getColorValue(parser, i); + if (activeColor == COLOR_NOT_SET) continue; + workingTab.setActiveColor(activeColor); + break; + case BAR_COLOR_WHEN_SELECTED: + int barColorWhenSelected = getColorValue(parser, i); + if (barColorWhenSelected == COLOR_NOT_SET) continue; + workingTab.setBarColorWhenSelected(barColorWhenSelected); + break; + case BADGE_BACKGROUND_COLOR: + int badgeBackgroundColor = getColorValue(parser, i); + if (badgeBackgroundColor == COLOR_NOT_SET) continue; + workingTab.setBadgeBackgroundColor(badgeBackgroundColor); + break; + case BADGE_HIDES_WHEN_ACTIVE: + boolean badgeHidesWhenActive = parser.getAttributeBooleanValue(i, true); + workingTab.setBadgeHidesWhenActive(badgeHidesWhenActive); + break; + case IS_TITLELESS: + boolean isTitleless = parser.getAttributeBooleanValue(i, false); + workingTab.setIsTitleless(isTitleless); + break; + } + } + + return workingTab; + } + + @NonNull + private BottomBarTab tabWithDefaults() { + BottomBarTab tab = new BottomBarTab(context); + tab.setConfig(defaultTabConfig); + + return tab; + } + + @NonNull + private String getTitleValue(@NonNull XmlResourceParser parser, @IntRange(from = 0) int attrIndex) { + int titleResource = parser.getAttributeResourceValue(attrIndex, 0); + return titleResource == RESOURCE_NOT_FOUND + ? parser.getAttributeValue(attrIndex) : context.getString(titleResource); + } + + @ColorInt + private int getColorValue(@NonNull XmlResourceParser parser, @IntRange(from = 0) int attrIndex) { + int colorResource = parser.getAttributeResourceValue(attrIndex, 0); + + if (colorResource == RESOURCE_NOT_FOUND) { + try { + String colorValue = parser.getAttributeValue(attrIndex); + return Color.parseColor(colorValue); + } catch (Exception ignored) { + return COLOR_NOT_SET; + } + } + + return ContextCompat.getColor(context, colorResource); + } + + @Retention(RetentionPolicy.SOURCE) + @StringDef({ + ID, + ICON, + TITLE, + INACTIVE_COLOR, + ACTIVE_COLOR, + BAR_COLOR_WHEN_SELECTED, + BADGE_BACKGROUND_COLOR, + BADGE_HIDES_WHEN_ACTIVE, + IS_TITLELESS + }) + @interface TabAttribute { + String ID = "id"; + String ICON = "icon"; + String TITLE = "title"; + String INACTIVE_COLOR = "inActiveColor"; + String ACTIVE_COLOR = "activeColor"; + String BAR_COLOR_WHEN_SELECTED = "barColorWhenSelected"; + String BADGE_BACKGROUND_COLOR = "badgeBackgroundColor"; + String BADGE_HIDES_WHEN_ACTIVE = "badgeHidesWhenActive"; + String IS_TITLELESS = "iconOnly"; + } + + @SuppressWarnings("WeakerAccess") + public static class TabParserException extends RuntimeException { + // This class is just to be able to have a type of Runtime Exception that will make it clear where the error originated. + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/TabSelectionInterceptor.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/TabSelectionInterceptor.java new file mode 100644 index 000000000..d510f6f8c --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/TabSelectionInterceptor.java @@ -0,0 +1,33 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +import androidx.annotation.IdRes; + +/* + * BottomBar library for Android + * Copyright (c) 2016 Iiro Krankka (http://github.com/roughike). + * + * 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. + */ +public interface TabSelectionInterceptor { + /** + * The method being called when currently visible {@link BottomBarTab} is about to change. + *

+ * This listener is fired when the current {@link BottomBar} is about to change. This gives + * an opportunity to interrupt the {@link BottomBarTab} change. + * + * @param oldTabId the currently visible {@link BottomBarTab} + * @param newTabId the {@link BottomBarTab} that will be switched to + * @return true if you want to override/stop the tab change, false to continue as normal + */ + boolean shouldInterceptTabSelection(@IdRes int oldTabId, @IdRes int newTabId); +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/VerticalScrollingBehavior.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/VerticalScrollingBehavior.java new file mode 100644 index 000000000..ab1e5bea8 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/VerticalScrollingBehavior.java @@ -0,0 +1,150 @@ +package com.habitrpg.android.habitica.ui.views.bottombar; + +import android.content.Context; +import android.os.Parcelable; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.WindowInsetsCompat; + +import android.util.AttributeSet; +import android.view.View; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by Nikola D. on 11/22/2015. + * + * Credit goes to Nikola Despotoski: + * https://github.com/NikolaDespotoski + */ +abstract class VerticalScrollingBehavior extends CoordinatorLayout.Behavior { + + private int totalDyUnconsumed = 0; + private int totalDy = 0; + @ScrollDirection + private int overScrollDirection = ScrollDirection.SCROLL_NONE; + @ScrollDirection + private int scrollDirection = ScrollDirection.SCROLL_NONE; + + VerticalScrollingBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + VerticalScrollingBehavior() { + super(); + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ScrollDirection.SCROLL_DIRECTION_UP, ScrollDirection.SCROLL_DIRECTION_DOWN, ScrollDirection.SCROLL_NONE}) + @interface ScrollDirection { + int SCROLL_DIRECTION_UP = 1; + int SCROLL_DIRECTION_DOWN = -1; + int SCROLL_NONE = 0; + } + + + /* + @return Overscroll direction: SCROLL_DIRECTION_UP, CROLL_DIRECTION_DOWN, SCROLL_NONE + */ + @ScrollDirection + int getOverScrollDirection() { + return overScrollDirection; + } + + + /** + * @return Scroll direction: SCROLL_DIRECTION_UP, SCROLL_DIRECTION_DOWN, SCROLL_NONE + */ + + @ScrollDirection + int getScrollDirection() { + return scrollDirection; + } + + + /** + * @param coordinatorLayout + * @param child + * @param direction Direction of the overscroll: SCROLL_DIRECTION_UP, SCROLL_DIRECTION_DOWN + * @param currentOverScroll Unconsumed value, negative or positive based on the direction; + * @param totalOverScroll Cumulative value for current direction + */ + abstract void onNestedVerticalOverScroll(CoordinatorLayout coordinatorLayout, V child, @ScrollDirection int direction, int currentOverScroll, int totalOverScroll); + + /** + * @param scrollDirection Direction of the overscroll: SCROLL_DIRECTION_UP, SCROLL_DIRECTION_DOWN + */ + abstract void onDirectionNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed, @ScrollDirection int scrollDirection); + + @Override + public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes) { + return (nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0; + } + + @Override + public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes) { + super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes); + } + + @Override + public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) { + super.onStopNestedScroll(coordinatorLayout, child, target); + } + + @Override + public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { + super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); + if (dyUnconsumed > 0 && totalDyUnconsumed < 0) { + totalDyUnconsumed = 0; + overScrollDirection = ScrollDirection.SCROLL_DIRECTION_UP; + } else if (dyUnconsumed < 0 && totalDyUnconsumed > 0) { + totalDyUnconsumed = 0; + overScrollDirection = ScrollDirection.SCROLL_DIRECTION_DOWN; + } + totalDyUnconsumed += dyUnconsumed; + onNestedVerticalOverScroll(coordinatorLayout, child, overScrollDirection, dyConsumed, totalDyUnconsumed); + } + + @Override + public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed) { + super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed); + if (dy > 0 && totalDy < 0) { + totalDy = 0; + scrollDirection = ScrollDirection.SCROLL_DIRECTION_UP; + } else if (dy < 0 && totalDy > 0) { + totalDy = 0; + scrollDirection = ScrollDirection.SCROLL_DIRECTION_DOWN; + } + totalDy += dy; + onDirectionNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, scrollDirection); + } + + + @Override + public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY, boolean consumed) { + super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed); + scrollDirection = velocityY > 0 ? ScrollDirection.SCROLL_DIRECTION_UP : ScrollDirection.SCROLL_DIRECTION_DOWN; + return onNestedDirectionFling(coordinatorLayout, child, target, velocityX, velocityY, scrollDirection); + } + + abstract boolean onNestedDirectionFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY, @ScrollDirection int scrollDirection); + + @Override + public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY) { + return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY); + } + + @NonNull + @Override + public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout, V child, WindowInsetsCompat insets) { + return super.onApplyWindowInsets(coordinatorLayout, child, insets); + } + + @Override + public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child) { + return super.onSaveInstanceState(parent, child); + } + +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/social/ChatBarView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/social/ChatBarView.kt index 31e2af0cc..0b011ed09 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/social/ChatBarView.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/social/ChatBarView.kt @@ -133,7 +133,7 @@ class ChatBarView : FrameLayout { navBarAccountedHeightCalculated = true val navbarHeight = NavbarUtils.getNavbarHeight(context) - spacing.updateLayoutParams { + spacing.updateLayoutParams { height = navbarHeight } }