I have a custom header with a custom behavior that interacting with a coordinator layout. This header depends on an appBarLayout that contains a collapsingToolbarLayout and a Toolbar. When the toolbar layout collapses, the custom header adjust its properties and position the way I want, but the second I reach the min height of the layout, the appBarLayout overlaps the custom header and I can't see it until I begin expanding it.
This is the code for the layout:
<?xml version="1.0" encoding="utf-8"?><android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="#+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="#dimen/mk_appbar_height">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/toolbar_collapse"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="#dimen/mt_toolbar_height"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleEnabled="false">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
style="#style/ToolbarStyle"
android:layout_width="match_parent"
android:layout_height="#dimen/mt_toolbar_height"
app:layout_collapseMode="pin"
/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<!-- This is a layout intended for containing MonkeyChatFragment and/or MonkeyConversationsFragment
RelativeLayout has an issue that doesn't render the RecyclerView with the whole size of its
container linear layout.
The only viable solution is FrameLayout -->
<FrameLayout
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"/>
<FrameLayout
android:id="#+id/viewStatus"
android:layout_width="match_parent"
android:layout_height="#dimen/status_height"
android:layout_marginTop="#dimen/status_inverse_height"
android:background="#color/mk_status_connected"/>
<com.criptext.monkeykitui.toolbar.HeaderView
android:id="#+id/custom_toolbar"
layout="#layout/custom_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="56dp"
app:layout_behavior="com.criptext.monkeykitui.toolbar.HeaderViewBehavior"
app:behavior_overlapTop="64dp"
/></android.support.design.widget.CoordinatorLayout>
and this is the code of the layout_behavior:
class HeaderViewBehavior(context: Context, attrs: AttributeSet? = null) : CoordinatorLayout.Behavior<HeaderView>(context, attrs){
private val MIN_AVATAR_PERCENTAGE_SIZE = 0.3f
private val EXTRA_FINAL_AVATAR_PADDING = 80
private val TAG = "behavior"
private val mContext: Context = context
private val mCustomFinalHeight: Float = 0.toFloat()
private var mStartToolbarPosition: Float = 0.toFloat()
private var mStartYPosition: Int = 0
private var mFinalYPosition: Int = 0
private var mStartHeight: Int = 0
private var mfontSize: Float = 20.toFloat()
private var mChangeBehaviorPoint: Float = 0.toFloat()
override fun layoutDependsOn(parent: CoordinatorLayout?, child: HeaderView?, dependency: View?): Boolean {
var hello = (dependency is AppBarLayout)
return hello
}
override fun onDependentViewChanged(parent: CoordinatorLayout, child: HeaderView, dependency: View): Boolean {
maybeInitProperties(child, dependency)
val maxScrollDistance = - mContext.resources.getDimension(R.dimen.mk_header_scroll)
val expandedPercentageFactor = dependency.y / maxScrollDistance
Log.d("TEST", dependency.y.toString())
Log.d("TEST", maxScrollDistance.toString())
if (expandedPercentageFactor < mChangeBehaviorPoint) {
val heightFactor = (mChangeBehaviorPoint - expandedPercentageFactor) / mChangeBehaviorPoint
val distanceYToSubtract = (mStartYPosition - mFinalYPosition) * (1f - expandedPercentageFactor) + child.getHeight() / 2
child.setY(mStartYPosition - distanceYToSubtract)
val heightToSubtract = (mStartHeight - mCustomFinalHeight) * heightFactor
val lp = child.layoutParams as CoordinatorLayout.LayoutParams
child.layoutParams = lp
} else {
val distanceYToSubtract = (mStartYPosition - mFinalYPosition) * (1f - expandedPercentageFactor)
child.setY(mStartYPosition - distanceYToSubtract)
if(mStartYPosition - distanceYToSubtract < mStartYPosition){
child.setY(mStartYPosition.toFloat())
}else if(mStartYPosition - distanceYToSubtract > mFinalYPosition){
child.setY(mFinalYPosition.toFloat())
}
child.title.textSize = mfontSize - (mfontSize - 25) * (1f - expandedPercentageFactor)
child.subtitle.textSize = 15 - (15 - 20) * (1f - expandedPercentageFactor)
child.imageView.layoutParams.height = (126 - (126 - 226) * (1f - expandedPercentageFactor)).toInt()
child.imageView.layoutParams.width = (126 - (126 - 226) * (1f - expandedPercentageFactor)).toInt()
}
return true
}
private fun maybeInitProperties(child: HeaderView, dependency: View) {
if (mStartYPosition === 0)
mStartYPosition = 0
if (mFinalYPosition === 0)
mFinalYPosition = mContext.resources.getDimension(R.dimen.mk_header_scroll).toInt()
if (mStartHeight === 0)
mStartHeight = child.getHeight()
if (mStartToolbarPosition === 0.toFloat())
mStartToolbarPosition = dependency.y
if (mChangeBehaviorPoint === 0.toFloat())
mChangeBehaviorPoint = (child.height - mCustomFinalHeight) / (2f * (mStartYPosition - mFinalYPosition))
}
}
Just in case anyone else is looking for the same answer I can say I've found solution in another SO question after literally wasting 6 hours trying various solutions to no avail.
To make View visibly overlay AppBarLayout after the latter has been collapsed you need to add android:elevation="8dp" (or higher value) property to the overlaying View.
So, the part of the code in the question that needs to change is:
<com.criptext.monkeykitui.toolbar.HeaderView
android:id="#+id/custom_toolbar"
layout="#layout/custom_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="56dp"
app:layout_behavior="com.criptext.monkeykitui.toolbar.HeaderViewBehavior"
app:behavior_overlapTop="64dp"
/>
to:
<com.criptext.monkeykitui.toolbar.HeaderView
android:id="#+id/custom_toolbar"
layout="#layout/custom_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="56dp"
app:layout_behavior="com.criptext.monkeykitui.toolbar.HeaderViewBehavior"
app:behavior_overlapTop="64dp"
android:elevation="8dp"
/>
All credits go to #kris larson's answer here: CoordinatorLayout custom behavior with AppBarLayout
Related
EDIT --- Adding the full code
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.camera.view.PreviewView
android:id="#+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:id="#+id/barsContainer"
android:layoutDirection="rtl"
app:layout_constraintBottom_toBottomOf="parent">
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
private var barsIndex = BooleanArray(30) { false }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
val layoutParams =
LinearLayout.LayoutParams(0, 80, 1f)
viewBinding.barsContainer.weightSum = (barsIndex.size).toFloat()
for (index in barsIndex.indices) {
val barView = View(this)
// layoutParams.setMargins(60, 60, 60, 60)
barView.setBackgroundColor(Color.parseColor("#FFFFFF"))
barView.layoutParams = layoutParams
viewBinding.barsContainer.addView(barView)
}
}
}
Well I've searched a lot and found a lot of answers regarding this question but still it has not worked for me so far.
I wanna add a number of bars with equal width to a LinearLayout.
this is what I got so far:
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:id="#+id/barsContainer"
android:layoutDirection="rtl"
app:layout_constraintBottom_toBottomOf="parent">
</androidx.appcompat.widget.LinearLayoutCompat>
and In my Activity.kt:
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
val layoutParams = LinearLayout.LayoutParams(0, 80, 1f)
for (index in barsIndex.indices) {
val barView = View(this)
barView.setBackgroundColor(Color.parseColor("#FFFFFF"))
barView.layoutParams = layoutParams
viewBinding.barsContainer.addView(barView, index)
}
viewBinding.barsContainer.requestLayout()
still it doesn't respect the weight (third parameter) and if I add some width it works but that's not what I want.
I tried both 0 and LinearLayout.LayoutParams.WRAP_CONTENT for the width it still does not work.
adding Margin to children doesn't work either!
Any help would be appreciated
i just edit some of your code to just edit color and height
you just need add this line befor the loop
barsContainer.weightSum = (barsIndex.size).toFloat()
To make sure that he takes the correct weights so that he divides the layout correctly as well
val barsContainer = findViewById<LinearLayoutCompat>(R.id.barsContainer)
val barsIndex = arrayListOf<Int>(1, 2, 3, 4)
barsContainer.weightSum = (barsIndex.size).toFloat()
val layoutParams = LinearLayoutCompat.LayoutParams(0, 150, 1f)
for (index in barsIndex.indices) {
val barView = View(this)
barView.setBackgroundColor(Color.parseColor("#ff${(index+1)*22}00"))
barView.layoutParams = layoutParams
barsContainer.addView(barView,index)
}
val barsIndex = arrayListOf<Int>(1, 2, 3, 4)
barsContainer.weightSum = (barsIndex.size).toFloat()
val layoutParams = LinearLayoutCompat.LayoutParams(0, 150, 1f)
layoutParams.marginEnd=1
layoutParams.marginStart=1
for (index in barsIndex.indices) {
val barView = View(this)
barView.setBackgroundColor(Color.parseColor("#ff${(index+1)*22}00"))
barView.layoutParams = layoutParams
barsContainer.addView(barView,index)
}
I have a CoordinatorLayout with a RecyclerView and a Fab button.
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/swipeChatRoomRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/chatRoomRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="true"
android:scrollbars="vertical" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/bottomScrollFab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="12dp"
android:src="#drawable/ic_arrow_down"
app:backgroundTint="#color/colorPrimaryLight"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:layout_anchor="#id/swipeChatRoomRecycler"
app:layout_anchorGravity="bottom|right|end"
app:fabSize="mini" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
The recyclerView is set as below
viewManager = LinearLayoutManager(activity).apply {
orientation = LinearLayoutManager.VERTICAL
stackFromEnd = false
reverseLayout = true
}
What i want to achieve is
At the launch of the activity i want the bottomScrollFab to be invisible since the recycler is scrolled in the end as by default
I want to achieve the exact reverse behaviour, meaning that i want the Fab to be shown when recycler is not onScroll and to be hidden when recycler is onScroll
How can i do those two things?
Well up to now i have created a custom Behaviour which changes the alpha when the recycler is scrolled. It works pretty fine
class ReverseHideViewOnScrollBehaviour(context: Context, attributeSet: AttributeSet): CoordinatorLayout.Behavior<View>(context, attributeSet) {
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
return dependency is RecyclerView
}
override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
val yPercentage = getViewOffsetFromRecycler(parent, child)
val diff = 100.0f - yPercentage
child.alpha = 1 - diff
return true
}
private fun getViewOffsetFromRecycler(parent: CoordinatorLayout, view: View): Float{
var percentage = 100f
val dependencies = parent.getDependencies(view)
dependencies.forEach { dependency ->
if (dependency is SwipeRefreshLayout) {
val recycler = (dependency.findViewById<RecyclerView>(R.id.chatRoomRecycler))
val offset: Int = recycler.computeVerticalScrollOffset()
val extent: Int = recycler.computeVerticalScrollExtent()
val range: Int = recycler.computeVerticalScrollRange()
percentage = if (range - extent == 0) 100f else 100.0f * offset / (range - extent).toFloat()
}
}
return percentage
}
}
So the FloatinActionButton
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/bottomScrollFab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="12dp"
android:src="#drawable/ic_arrow_down"
app:layout_behavior=".ui.views.ReverseHideViewOnScrollBehaviour"
app:layout_anchor="#id/swipeChatRoomRecycler"
app:layout_anchorGravity="bottom|right|end"
app:fabSize="mini" />
I have a problem with scrolling, AppBarLayout not fully scrolls to out of screen.
How can i to continue scroll to statusbar height?
But if i set to root layout (CoordinatorLayout) fitsSystemWIndows=false, no problem with scroll, but status bar becomes without alpha channel.
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/search_appbar"
android:layout_width="match_parent"
android:layout_height="50dp">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
...
other layouts here
...
</LinearLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Add Scrolling behaviour to your Toolbar which is inside AppbarLayout.
app:layout_scrollFlags="scroll|enterAlways"
EDIT:- Replace scrolling behaviour of CollapsingToolbarLayout with below code.
app:layout_scrollFlags="scroll|exitUntilCollapsed"
I found solution.
Maybe someone it helps.
create package: com.google.android.material.appbar
create class which extend AppBarLayout.Behavior
Result in the image
package com.google.android.material.appbar
import ...
class OffsetBehavior(c: Context, a: AttributeSet) : AppBarLayout.Behavior(c, a)
{
private var mOffset = 0
fun setOffset(value: Int) {
mOffset = value
}
// completely copy the method onNestedPreScroll from AppBarLaout.Behavior
// and add some changes
override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, child: AppBarLayout, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int)
{
if (dy != 0) {
val min: Int
val max: Int
// scroll down
if (dy < 0) {
min = -(child.totalScrollRange + mOffset)
max = min + child.downNestedPreScrollRange + mOffset
}
// scroll up
else {
min = -(child.upNestedPreScrollRange + mOffset)
max = 0
}
if (min != max) {
consumed[1] = scroll(coordinatorLayout, child, dy, min, max)
}
}
if (child.isLiftOnScroll) {
child.setLiftedState(child.shouldLift(target))
}
}
}
and then set it to AppBarLayout in XML
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/search_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.appbar.OffsetBehavior">
...
</com.google.android.material.appbar.AppBarLayout>
in the Activity:
(1) add fun
// getting statusbar height
// my phone with Android9 = 96px
// my phone with Android7 = 72px
fun getStatusBarHeight(): Int {
val id = resources.getIdentifier("status_bar_height", "dimen", "android")
return if (id > 0) {
resources.getDimensionPixelSize(id)
} else {
0
}
}
(2) in onCreate()
val lp = search_appbar?.layoutParams as? CoordinatorLayout.LayoutParams
val bh = lp?.behavior as? com.google.android.material.appbar.OffsetBehavior
bh?.setOffset(getStatusBarHeight())
Result:
https://i.stack.imgur.com/pNeze.gif
I am implementing expanding and collapsing toolbar with the help of collapsing toolbar but I am stuck when my toolbar is collapsed I want to show different toolbar. I have seen so piece of code but cannot be able to find my solution.
I have also seen the solution of one of the amazing developer https://github.com/saulmm/CoordinatorLayoutExample but cannot be able to find out my solution properly
This is my piece of code which i have implemented
activity_collapsing_toolbar.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="176dp"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="#color/base_color_theme_new"
android:gravity="center_horizontal"
app:layout_collapseMode="parallax">
<RelativeLayout
android:id="#+id/rl_class_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="26dp"
android:gravity="center">
<LinearLayout
android:id="#+id/ll_class"
android:layout_width="60dp"
android:layout_height="60dp"
android:background="#drawable/rounded_white_circle"
android:gravity="center">
<ImageView
android:id="#+id/iv_class_image"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="center"
android:padding="8dp"
android:src="#drawable/class_4" />
</LinearLayout>
</RelativeLayout>
<TextView
android:id="#+id/tv_class_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/rl_class_image"
android:layout_marginTop="15dp"
android:gravity="center"
android:text="MATHEMATICS"
android:textSize="17sp" />
<TextView
android:id="#+id/tv_videos_test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/tv_class_name"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="20 VIDEOS | 5 TESTS"
android:textSize="10sp" />
</RelativeLayout>
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:background="#drawable/rounded_corners_for_list"
android:fillViewport="true"
app:behavior_overlapTop="10dp"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<!--<include layout="#layout/activity_chapters" />-->
<com.chalklit.widget.NonScrollListView
android:id="#+id/lv_modules_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/white"
android:divider="#null"
android:scrollbars="none"></com.chalklit.widget.NonScrollListView>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
CollapsingToolbarActivity.java
private CollapsingToolbarLayout collapsingToolbarLayout = null;
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_collapsing_toolbar);
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.inflateMenu(R.menu.menu_main);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
collapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
collapsingToolbarLayout.setTitle(" ");
collapsingToolbarLayout.setContentScrimColor(getResources().getColor(R.color.base_color_theme_new));
collapsingToolbarLayout.setStatusBarScrimColor(getResources().getColor(R.color.base_color_theme_new));
}
I have preperead two amaizing avatar collapsing demo samples with approach that doesn’t use a custom CoordinatorLayoutBehavior!
To view my samples native code: "Collapsing Avatar Toolbar Sample"
To read my "Animation Collapsing Toolbar Android" post on Medium.
demo 1 demo 2
Instead of use use a custom CoordinatorLayoutBehavior i use an OnOffsetChangedListener which comes from AppBarLayout.
private lateinit var appBarLayout: AppBarLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_demo_1)
...
appBarLayout = findViewById(R.id.app_bar_layout)
/**/
appBarLayout.addOnOffsetChangedListener(
AppBarLayout.OnOffsetChangedListener { appBarLayout, i ->
...
/**/
updateViews(Math.abs(i / appBarLayout.totalScrollRange.toFloat()))
})
}
Demo 1
in updateViews method avatar changes the size and changes avatar’s X, Y position translation in first demo.
private fun updateViews(offset: Float) {
...
/* Collapse avatar img*/
ivUserAvatar.apply {
when {
offset > avatarAnimateStartPointY -> {
val avatarCollapseAnimateOffset = (offset - avatarAnimateStartPointY) * avatarCollapseAnimationChangeWeight
val avatarSize = EXPAND_AVATAR_SIZE - (EXPAND_AVATAR_SIZE - COLLAPSE_IMAGE_SIZE) * avatarCollapseAnimateOffset
this.layoutParams.also {
it.height = Math.round(avatarSize)
it.width = Math.round(avatarSize)
}
invisibleTextViewWorkAround.setTextSize(TypedValue.COMPLEX_UNIT_PX, offset)
this.translationX = ((appBarLayout.width - horizontalToolbarAvatarMargin - avatarSize) / 2) * avatarCollapseAnimateOffset
this.translationY = ((toolbar.height - verticalToolbarAvatarMargin - avatarSize ) / 2) * avatarCollapseAnimateOffset
}
else -> this.layoutParams.also {
if (it.height != EXPAND_AVATAR_SIZE.toInt()) {
it.height = EXPAND_AVATAR_SIZE.toInt()
it.width = EXPAND_AVATAR_SIZE.toInt()
this.layoutParams = it
}
translationX = 0f
}
}
}
}
to find avatarAnimateStartPointY and avatarCollapseAnimationChangeWeight (for convert general offset to avatar animate offset):
private var avatarAnimateStartPointY: Float = 0F
private var avatarCollapseAnimationChangeWeight: Float = 0F
private var isCalculated = false
private var verticalToolbarAvatarMargin =0F
...
if (isCalculated.not()) {
avatarAnimateStartPointY =
Math.abs((appBarLayout.height - (EXPAND_AVATAR_SIZE + horizontalToolbarAvatarMargin)) / appBarLayout.totalScrollRange)
avatarCollapseAnimationChangeWeight = 1 / (1 - avatarAnimateStartPointY)
verticalToolbarAvatarMargin = (toolbar.height - COLLAPSE_IMAGE_SIZE) * 2
isCalculated = true
}
Demo 2
avatar change his size and than animate move to right at one moment with top toolbar text became to show and moving to left.
You need to track states: TO_EXPANDED_STATE changing, TO_COLLAPSED_STATE changing, WAIT_FOR_SWITCH.
/*Collapsed/expended sizes for views*/
val result: Pair<Int, Int> = when {
percentOffset < ABROAD -> {
Pair(TO_EXPANDED_STATE, cashCollapseState?.second ?: WAIT_FOR_SWITCH)
}
else -> {
Pair(TO_COLLAPSED_STATE, cashCollapseState?.second ?: WAIT_FOR_SWITCH)
}
}
Create animation for avatar on state switch change:
result.apply {
var translationY = 0f
var headContainerHeight = 0f
val translationX: Float
var currentImageSize = 0
when {
cashCollapseState != null && cashCollapseState != this -> {
when (first) {
TO_EXPANDED_STATE -> {
translationY = toolbar.height.toFloat()
headContainerHeight = appBarLayout.totalScrollRange.toFloat()
currentImageSize = EXPAND_AVATAR_SIZE.toInt()
/**/
titleToolbarText.visibility = View.VISIBLE
titleToolbarTextSingle.visibility = View.INVISIBLE
background.setBackgroundColor(ContextCompat.getColor(this#Demo2Activity, R.color.color_transparent))
/**/
ivAvatar.translationX = 0f
}
TO_COLLAPSED_STATE -> {
background.setBackgroundColor(ContextCompat.getColor(this#Demo2Activity, R.color.colorPrimary))
currentImageSize = COLLAPSE_IMAGE_SIZE.toInt()
translationY = appBarLayout.totalScrollRange.toFloat() - (toolbar.height - COLLAPSE_IMAGE_SIZE) / 2
headContainerHeight = toolbar.height.toFloat()
translationX = appBarLayout.width / 2f - COLLAPSE_IMAGE_SIZE / 2 - margin * 2
/**/
ValueAnimator.ofFloat(ivAvatar.translationX, translationX).apply {
addUpdateListener {
if (cashCollapseState!!.first == TO_COLLAPSED_STATE) {
ivAvatar.translationX = it.animatedValue as Float
}
}
interpolator = AnticipateOvershootInterpolator()
startDelay = 69
duration = 350
start()
}
...
}
}
ivAvatar.apply {
layoutParams.height = currentImageSize
layoutParams.width = currentImageSize
}
collapsingAvatarContainer.apply {
layoutParams.height = headContainerHeight.toInt()
this.translationY = translationY
requestLayout()
}
/**/
cashCollapseState = Pair(first, SWITCHED)
}
To view my samples native code: "Collapsing Avatar Toolbar Sample"
Here's another approach that doesn't use a custom CoordinatorLayoutBehavior.
It uses an OnOffsetChangedListener which comes from AppBarLayout.
Here's a snippet:
class OnOffsetChangedListener implements AppBarLayout.OnOffsetChangedListener {
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
final int scrollRange = appBarLayout.getTotalScrollRange();
float offsetFactor = (float) (-verticalOffset) / (float) scrollRange;
...
This shows you how to find the total scroll range and then find the ratio between the total scroll range and the current scroll position. This is what you need to figure out how to scale and position your toolbar views.
For a custom layout (like I did), you can override onAttachedToWindow and add the listener there:
// Add an OnOffsetChangedListener if possible
final ViewParent parent = getParent();
if (parent instanceof AppBarLayout) {
if (mOnOffsetChangedListener == null) {
mOnOffsetChangedListener = new OnOffsetChangedListener();
}
((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
}
I found this approach to be a little simpler than creating a custom behavior.
I created an example project on GitHub. The app looks like this:
You can see the whole project at https://github.com/klarson2/Collapsing-Image
you should add Line #33
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbar_layout"
android:layout_width="match_parent"
android:layout_height="192dp"
android:fitsSystemWindows="true"
android:theme="#style/AppTheme">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|snap|exitUntilCollapsed"
app:title="Collapsing"
app:toolbarId="#+id/toolbar">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="#drawable/nana"
app:layout_collapseMode="parallax" />
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="#drawable/ax" />
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/floating_action_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:baselineAlignBottom="false"
android:clickable="true"
android:src="#drawable/possetive"
app:fabSize="normal"
app:layout_anchor="#id/appbar_layout"
app:layout_anchorGravity="bottom|right"
app:rippleColor="#E4D6D6" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
To achieve this, we must have to create custom behavior using CoordinatorLayout.Behavior
Take into account two core elements: child and dependency:
The child is the view that enhances behavior, dependency who will serve as a trigger to interact with the child element. In your requirement the child is the ImageView and the dependency is the Toolbar, in that way, if the Toolbar moves, the ImageView will move too.
Please check some below links for custome behaviour toolbar demos
http://www.devexchanges.info/2016/03/android-tip-custom-coordinatorlayout.html
https://medium.com/google-developers/intercepting-everything-with-coordinatorlayout-behaviors-8c6adc140c26#.tfsd7ftkl
I want do this but with Collapsing toolbar layout or display the logo and title in toolbar after scroll.
<!-- Toolbars -->
<android.support.design.widget.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="#dimen/detail_backdrop_height"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:fitsSystemWindows="true">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginStart="48dp"
app:expandedTitleMarginEnd="64dp"
android:fitsSystemWindows="true">
<ImageView
android:id="#+id/background_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="#drawable/background_1"
app:layout_collapseMode="parallax"
android:fitsSystemWindows="true"/>
<RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<ImageView
android:id="#+id/avatar_image"
android:layout_width="#dimen/circular_image_avatar"
android:layout_height="#dimen/circular_image_avatar"
android:gravity="center"
android:scaleType="centerCrop"
android:src="#drawable/ic_placerholder"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:transitionName="image_toolbar"/>
<TextView
android:id="#+id/profile_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Name title"
android:textAlignment="center"
android:layout_marginTop="#dimen/item_padding_top_bottom"
android:gravity="center"
style="#style/titleText_toolbar"
android:layout_below="#+id/avatar_image"
android:transitionName="title_toolbar"/>
<TextView
android:id="#+id/profile_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Subtitle"
android:textAlignment="center"
android:gravity="center"
style="#style/captionText_toolbar"
android:layout_below="#+id/profile_title" />
</RelativeLayout>
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light"
app:layout_collapseMode="pin">
<!-- avatar image and title, subtitle -->
</android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
Please help me
I have preperead two amaizing avatar collapsing demo samples with approach that doesn’t use a custom CoordinatorLayoutBehavior!
To view my samples native code: "Collapsing Avatar Toolbar Sample"
To read my "Animation Collapsing Toolbar Android" post on Medium.
demo 1 demo 2
Instead of use use a custom CoordinatorLayoutBehavior i use an OnOffsetChangedListener which comes from AppBarLayout.
private lateinit var appBarLayout: AppBarLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_demo_1)
...
appBarLayout = findViewById(R.id.app_bar_layout)
/**/
appBarLayout.addOnOffsetChangedListener(
AppBarLayout.OnOffsetChangedListener { appBarLayout, i ->
...
/**/
updateViews(Math.abs(i / appBarLayout.totalScrollRange.toFloat()))
})
}
Demo 1
in updateViews method avatar changes the size and changes avatar’s X, Y position translation in first demo.
private fun updateViews(offset: Float) {
...
/* Collapse avatar img*/
ivUserAvatar.apply {
when {
offset > avatarAnimateStartPointY -> {
val avatarCollapseAnimateOffset = (offset - avatarAnimateStartPointY) * avatarCollapseAnimationChangeWeight
val avatarSize = EXPAND_AVATAR_SIZE - (EXPAND_AVATAR_SIZE - COLLAPSE_IMAGE_SIZE) * avatarCollapseAnimateOffset
this.layoutParams.also {
it.height = Math.round(avatarSize)
it.width = Math.round(avatarSize)
}
invisibleTextViewWorkAround.setTextSize(TypedValue.COMPLEX_UNIT_PX, offset)
this.translationX = ((appBarLayout.width - horizontalToolbarAvatarMargin - avatarSize) / 2) * avatarCollapseAnimateOffset
this.translationY = ((toolbar.height - verticalToolbarAvatarMargin - avatarSize ) / 2) * avatarCollapseAnimateOffset
}
else -> this.layoutParams.also {
if (it.height != EXPAND_AVATAR_SIZE.toInt()) {
it.height = EXPAND_AVATAR_SIZE.toInt()
it.width = EXPAND_AVATAR_SIZE.toInt()
this.layoutParams = it
}
translationX = 0f
}
}
}
}
to find avatarAnimateStartPointY and avatarCollapseAnimationChangeWeight (for convert general offset to avatar animate offset):
private var avatarAnimateStartPointY: Float = 0F
private var avatarCollapseAnimationChangeWeight: Float = 0F
private var isCalculated = false
private var verticalToolbarAvatarMargin =0F
...
if (isCalculated.not()) {
avatarAnimateStartPointY =
Math.abs((appBarLayout.height - (EXPAND_AVATAR_SIZE + horizontalToolbarAvatarMargin)) / appBarLayout.totalScrollRange)
avatarCollapseAnimationChangeWeight = 1 / (1 - avatarAnimateStartPointY)
verticalToolbarAvatarMargin = (toolbar.height - COLLAPSE_IMAGE_SIZE) * 2
isCalculated = true
}
Demo 2
avatar change his size and than animate move to right at one moment with top toolbar text became to show and moving to left.
You need to track states: TO_EXPANDED_STATE changing, TO_COLLAPSED_STATE changing, WAIT_FOR_SWITCH.
/*Collapsed/expended sizes for views*/
val result: Pair<Int, Int> = when {
percentOffset < ABROAD -> {
Pair(TO_EXPANDED_STATE, cashCollapseState?.second ?: WAIT_FOR_SWITCH)
}
else -> {
Pair(TO_COLLAPSED_STATE, cashCollapseState?.second ?: WAIT_FOR_SWITCH)
}
}
Create animation for avatar on state switch change:
result.apply {
var translationY = 0f
var headContainerHeight = 0f
val translationX: Float
var currentImageSize = 0
when {
cashCollapseState != null && cashCollapseState != this -> {
when (first) {
TO_EXPANDED_STATE -> {
translationY = toolbar.height.toFloat()
headContainerHeight = appBarLayout.totalScrollRange.toFloat()
currentImageSize = EXPAND_AVATAR_SIZE.toInt()
/**/
titleToolbarText.visibility = View.VISIBLE
titleToolbarTextSingle.visibility = View.INVISIBLE
background.setBackgroundColor(ContextCompat.getColor(this#Demo2Activity, R.color.color_transparent))
/**/
ivAvatar.translationX = 0f
}
TO_COLLAPSED_STATE -> {
background.setBackgroundColor(ContextCompat.getColor(this#Demo2Activity, R.color.colorPrimary))
currentImageSize = COLLAPSE_IMAGE_SIZE.toInt()
translationY = appBarLayout.totalScrollRange.toFloat() - (toolbar.height - COLLAPSE_IMAGE_SIZE) / 2
headContainerHeight = toolbar.height.toFloat()
translationX = appBarLayout.width / 2f - COLLAPSE_IMAGE_SIZE / 2 - margin * 2
/**/
ValueAnimator.ofFloat(ivAvatar.translationX, translationX).apply {
addUpdateListener {
if (cashCollapseState!!.first == TO_COLLAPSED_STATE) {
ivAvatar.translationX = it.animatedValue as Float
}
}
interpolator = AnticipateOvershootInterpolator()
startDelay = 69
duration = 350
start()
}
...
}
}
ivAvatar.apply {
layoutParams.height = currentImageSize
layoutParams.width = currentImageSize
}
collapsingAvatarContainer.apply {
layoutParams.height = headContainerHeight.toInt()
this.translationY = translationY
requestLayout()
}
/**/
cashCollapseState = Pair(first, SWITCHED)
}
To view my samples native code: "Collapsing Avatar Toolbar Sample"
I think these type of animations can be achieved easily using MotionLayout. I have implemented sample collapsing layout using MotionLayout here. You can modify it for your use case. Simple change the start and end constraints.