Custom shape of bottom navigation view (Android)? - android

How to make the bottom navigation view to a specific shape?
I'd like to have a bottom navigation view of this shape:
I have tried setting it as background of my bottom nav view as:
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/navigationBottomView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#drawable/bg_nav_bar"
app:itemHorizontalTranslationEnabled="true"
app:itemIconTint="#drawable/bottom_bar_selector"
app:itemTextColor="#drawable/bottom_bar_selector"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/nav_menu"/>
But it doesn't seem to work.
Any help will be appreciated. Thanks!

The BottomNavigationView by default has a background of MaterialShapeDrawable so you can change its shape using the ShapeAppearanceModel by defining a custom TopEdge EdgeTreatment to draw the half-circle above the BottomNavigationView. To be able to draw something above the BottomNavigationView you need to have a parent which has the below attributes:
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="35dp"
An Xml sample will be like the below:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/black">
<RelativeLayout
android:id="#+id/bottomNavigationViewParentRL"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="35dp"
android:background="#android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:backgroundTint="#color/white"
app:elevation="2dp"
app:labelVisibilityMode="labeled"
app:itemIconSize="25dp"
app:itemIconTint="#color/item_icon_tint_selector"
app:itemTextColor="#color/item_text_color_selector"
app:menu="#menu/bottom_nav_menu" />
</RelativeLayout>
<fragment
android:id="#+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="#id/bottomNavigationViewParentRL"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Then draw the shape like the below:
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottomNavigationView)
val materialShapeDrawable = bottomNavigationView.getBackground() as MaterialShapeDrawable
materialShapeDrawable.shapeAppearanceModel = materialShapeDrawable.shapeAppearanceModel
.toBuilder()
.setTopEdge(CutoutCircleEdgeTreatment(resources, 70.toFloat(), 10.toFloat()))
.build()
where CutoutCircleEdgeTreatment is a subclass of EdgeTreatment to draw the half-circle at the top which is similar code like the build-in BottomAppBarTopEdgeTreatment class which draws a semi-circular cutout from the top edge to bottom:
class CutoutCircleEdgeTreatment(res: Resources, circleDiameterDp: Float, circleLeftRightOffsetDp: Float) : EdgeTreatment() {
private val fabDiameter: Float
private val offset: Float
init {
fabDiameter = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleDiameterDp, res.getDisplayMetrics())
offset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleLeftRightOffsetDp, res.getDisplayMetrics())
}
override fun getEdgePath(length: Float, center: Float, interpolation: Float, shapePath: ShapePath) {
if (fabDiameter == 0f) {
// There is no cutout to draw.
shapePath.lineTo(length, 0f)
return
}
val fabMargin = 0f
val cradleDiameter = fabMargin * 2 + fabDiameter
val cradleRadius = cradleDiameter / 2f
val roundedCornerRadius = 0f
val roundedCornerOffset = interpolation * roundedCornerRadius
val horizontalOffset = 0f
val middle = center + horizontalOffset
// The center offset of the cutout tweens between the vertical offset when attached, and the
// cradleRadius as it becomes detached.
val cradleVerticalOffset = 0f
val verticalOffset =
interpolation * cradleVerticalOffset + (1 - interpolation) * cradleRadius
val verticalOffsetRatio = verticalOffset / cradleRadius
if (verticalOffsetRatio >= 1.0f) {
// Vertical offset is so high that there's no curve to draw in the edge, i.e., the fab is
// actually above the edge so just draw a straight line.
shapePath.lineTo(length, 0f)
return // Early exit.
}
// Calculate the path of the cutout by calculating the location of two adjacent circles. One
// circle is for the rounded corner. If the rounded corner circle radius is 0 the corner will
// not be rounded. The other circle is the cutout.
// Calculate the X distance between the center of the two adjacent circles using pythagorean
// theorem.
val fabCornerSize = -1f
val cornerSize = fabCornerSize * interpolation
val arcOffset = 0f
val distanceBetweenCenters = cradleRadius + roundedCornerOffset
val distanceBetweenCentersSquared = distanceBetweenCenters * distanceBetweenCenters
val distanceY = verticalOffset + roundedCornerOffset
val distanceX =
Math.sqrt((distanceBetweenCentersSquared - distanceY * distanceY).toDouble())
.toFloat()
// Calculate the x position of the rounded corner circles.
val leftRoundedCornerCircleX = middle - distanceX
val rightRoundedCornerCircleX = middle + distanceX
// Calculate the arc between the center of the two circles.
val cornerRadiusArcLength =
Math.toDegrees(Math.atan((distanceX / distanceY).toDouble())).toFloat()
val cutoutArcOffset = ARC_QUARTER - cornerRadiusArcLength + arcOffset
// Draw the starting line up to the left rounded corner.
shapePath.lineTo( /* x= */leftRoundedCornerCircleX, 0f)
// Draw the arc for the left rounded corner circle. The bounding box is the area around the
// circle's center which is at `(leftRoundedCornerCircleX, roundedCornerOffset)`.
shapePath.addArc( /* left= */
leftRoundedCornerCircleX - roundedCornerOffset, 0f, /* right= */
leftRoundedCornerCircleX + roundedCornerOffset, /* bottom= */
roundedCornerOffset * 2, /* startAngle= */
ANGLE_UP.toFloat(), /* sweepAngle= */
cornerRadiusArcLength
)
// Draw the cutout circle.
shapePath.addArc( /* left= */
middle - (cradleRadius + offset), /* top= */
-cradleRadius - verticalOffset, /* right= */
middle + (cradleRadius + offset), /* bottom= */
cradleRadius - verticalOffset, /* startAngle= */
ANGLE_LEFT - cutoutArcOffset, /* sweepAngle= */
cutoutArcOffset * 2 + ARC_HALF
)
// Draw an arc for the right rounded corner circle. The bounding box is the area around the
// circle's center which is at `(rightRoundedCornerCircleX, roundedCornerOffset)`.
shapePath.addArc( /* left= */
rightRoundedCornerCircleX - roundedCornerOffset, 0f, /* right= */
rightRoundedCornerCircleX + roundedCornerOffset, /* bottom= */
roundedCornerOffset * 2, /* startAngle= */
ANGLE_UP - cornerRadiusArcLength, /* sweepAngle= */
cornerRadiusArcLength
)
// Draw the ending line after the right rounded corner.
shapePath.lineTo( /* x= */length, 0f)
}
companion object {
private const val ARC_QUARTER = 90
private const val ARC_HALF = 180
private const val ANGLE_UP = 270
private const val ANGLE_LEFT = 180
}
}
From the above CutoutCircleEdgeTreatment constructor you can pass the circleDiameterDp which is the circle diameter in dp value (in the above example is set to 70dp so the parent RelativeLayout it should have paddingTop equal to the radius of the Circle which is 70/2 = 35dp) and the circleLeftRightOffsetDp is used to draw the circle with a left/right offset in dp value. Of Course you can modify further the code based on your needs.
Result:
To overlap the semi circle with the fragment hosted
To make the semi circle overlap with the fragment hosted you have to change the order of fragment:nav_host_fragment_activity_main with the RelativeLayout bottomNavigationViewParentRL like in the below sample:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/black">
<fragment
android:id="#+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="#id/bottomNavigationViewParentRL"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/mobile_navigation" />
<RelativeLayout
android:id="#+id/bottomNavigationViewParentRL"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="35dp"
android:background="#android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:backgroundTint="#color/white"
app:elevation="2dp"
app:labelVisibilityMode="labeled"
app:itemIconSize="25dp"
app:itemIconTint="#color/item_icon_tint_selector"
app:itemTextColor="#color/item_text_color_selector"
app:menu="#menu/bottom_nav_menu" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
And also give in each of your fragments some bottom margin with the same height of the navigation bar to start at the point of semi circle like in the below sample:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:background="#android:color/transparent"
tools:context=".ui.dashboard.DashboardFragment">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/holo_green_dark"
android:layout_marginBottom="55dp">
<TextView
android:id="#+id/text_dashboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="25dp"
android:textAlignment="center"
android:textColor="#color/black"
android:text="This is dashboard Fragment"
android:textSize="20sp"
android:layout_alignParentBottom="true"/>
</RelativeLayout>
</RelativeLayout>
Result:
Another variation of CutoutCircleEdgeTreatment
class CutoutCircleEdgeTreatment(res: Resources, circleDiameterDp: Float, circleLeftRightOffsetDp: Float) : EdgeTreatment() {
private val fabDiameter: Float
private val offset: Float
init {
fabDiameter = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleDiameterDp, res.getDisplayMetrics())
offset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleLeftRightOffsetDp, res.getDisplayMetrics())
}
override fun getEdgePath(length: Float, center: Float, interpolation: Float, shapePath: ShapePath) {
if (fabDiameter == 0f) {
// There is no cutout to draw.
shapePath.lineTo(length, 0f)
return
}
val fabMargin = 0f
val cradleDiameter = fabMargin * 2 + fabDiameter
val cradleRadius = cradleDiameter / 2f
val roundedCornerRadius = 0f
val roundedCornerOffset = interpolation * roundedCornerRadius
val horizontalOffset = 0f
val middle = center + horizontalOffset
// The center offset of the cutout tweens between the vertical offset when attached, and the
// cradleRadius as it becomes detached.
val cradleVerticalOffset = 0f
val verticalOffset =
interpolation * cradleVerticalOffset + (1 - interpolation) * cradleRadius
val verticalOffsetRatio = verticalOffset / cradleRadius
if (verticalOffsetRatio >= 1.0f) {
// Vertical offset is so high that there's no curve to draw in the edge, i.e., the fab is
// actually above the edge so just draw a straight line.
shapePath.lineTo(length, 0f)
return // Early exit.
}
// Calculate the path of the cutout by calculating the location of two adjacent circles. One
// circle is for the rounded corner. If the rounded corner circle radius is 0 the corner will
// not be rounded. The other circle is the cutout.
// Calculate the X distance between the center of the two adjacent circles using pythagorean
// theorem.
val fabCornerSize = -1f
val cornerSize = fabCornerSize * interpolation
val arcOffset = 0f
val distanceBetweenCenters = cradleRadius + roundedCornerOffset
val distanceBetweenCentersSquared = distanceBetweenCenters * distanceBetweenCenters
val distanceY = verticalOffset + roundedCornerOffset
val distanceX =
Math.sqrt((distanceBetweenCentersSquared - distanceY * distanceY).toDouble())
.toFloat()
// Calculate the x position of the rounded corner circles.
val leftRoundedCornerCircleX = middle - distanceX
val rightRoundedCornerCircleX = middle + distanceX
// Calculate the arc between the center of the two circles.
val cornerRadiusArcLength =
Math.toDegrees(Math.atan((distanceX / distanceY).toDouble())).toFloat()
val cutoutArcOffset = ARC_QUARTER - cornerRadiusArcLength + arcOffset
// Draw the cutout circle.
shapePath.addArc( /* left= */
middle - (cradleRadius + offset), /* top= */
-cradleRadius - verticalOffset, /* right= */
middle + (cradleRadius + offset), /* bottom= */
(cradleRadius - verticalOffset) * 2, /* startAngle= */
ANGLE_LEFT + 20.0f, /* sweepAngle= */
ARC_HALF - 40.0f
)
}
companion object {
private const val ARC_QUARTER = 90
private const val ARC_HALF = 180
private const val ANGLE_UP = 270
private const val ANGLE_LEFT = 180
}
}
Usage:
val materialShapeDrawable = bottomNavigationView.getBackground() as MaterialShapeDrawable
materialShapeDrawable.shapeAppearanceModel = materialShapeDrawable.shapeAppearanceModel
.toBuilder()
.setTopEdge(CutoutCircleEdgeTreatment(resources, 70.toFloat(), 20.toFloat()))
.build()
Result:

Related

Is there a way to draw a circle with the starting coordinates off screen in jetpack compose?

I'm trying to create a screen with a large semi circle background like the following image in compose.
image
In the XML based layout I was able to do it by scaling a circle to twice the width using layout_constraintWidth_percent="2" but I'm struggling to figure out how to do this in compose. Is there a way to draw a circle larger than the screen and use it as a background?
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:background="#color/red_400">
<androidx.constraintlayout.widget.Guideline
android:id="#+id/topGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.25" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#drawable/shape_oval"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/topGuideline"
app:layout_constraintWidth_percent="2" />
</androidx.constraintlayout.widget.ConstraintLayout>
You draw oval at 25% of height of your view and scale width by factor of 2 which can be done with Compose as
#Composable
private fun CircleBox(modifier: Modifier = Modifier, content: #Composable () -> Unit) {
Box(
modifier = modifier.drawBehind {
val width = size.width
val height = size.height
drawRect(Color.Red)
scale(scaleX = 2f, scaleY = 1f) {
drawOval(
Color.White,
topLeft = Offset(0f, height * .25f),
size = Size(width, height)
)
}
// Bonus Dashed line at 25% of height
drawLine(
Color.LightGray,
strokeWidth = 2.dp.toPx(),
start = Offset(0f, height * .25f),
end = Offset(width, height * .25f),
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(20f, 20f)
)
)
}
) {
content()
}
}
Usage
CircleBox(modifier = Modifier.fillMaxSize()){
// Your Composables here
}
Result

Shadow of a custom shape

I have to implement a banner according to the following designs:
The complexity here is in the shadow of the round logo, the shadow of the logo circle is the continuation of the shadow of the rectangular card of the banner. The border of the shadow is outlined in the following image:
Of course the shadow shouldn't be casted on the surface below the top side of the card. Also the logo center has some offset from the border of the card.
How can I achieve this effect? Standard android shapes doesn't allow to form such a contour. Drawing everything manually seems to be too complex decision for the problem. We have minSdkVersion 21.
You can achieve it using this trick
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="20dp"
app:cardCornerRadius="56dp"
app:cardElevation="16dp"
android:layout_width="56dp"
android:layout_height="56dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="match_parent"
android:src="#color/colorPrimary"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
app:cardElevation="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="48dp"
android:layout_width="350dp"
android:layout_height="500dp">
<LinearLayout
android:layout_marginTop="-28dp"
android:layout_gravity="center_horizontal"
android:layout_width="56dp"
android:layout_height="56dp">
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="match_parent"
android:src="#color/colorPrimary"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
The result will be:
The main idea create two cardview with images, one under the main card and another one in the cardview and using margins make the look like one circle.
I've ended up on using the MaterialShapeDrawable class. It allows to customize drawable edges by manually defining how the edge should de drawn. This is achieved by deriving EdgeTreatment class (similar is possible for corners with CornerTreatment).
As a result the background of the banner looks like this:
Here is the banner top edge treatment class:
private class BannerTopEdgeTreatment(private val circleRadius: Int,
private val circleCenterOffset: Int) : EdgeTreatment(), Cloneable {
init {
// do not allow to offset circle center up
if (circleCenterOffset < 0)
throw IllegalArgumentException()
}
override fun getEdgePath(length: Float, center: Float, interpolation: Float, shapePath: ShapePath) {
// use interpolated radius
val radius = circleRadius * interpolation
// if circle lays entirely inside the rectangle then just draw a line
val circleTop = circleCenterOffset - radius
if (circleTop >= 0) {
shapePath.lineTo(length, 0f)
return
}
// calc the distance from the center of the edge to the point where arc begins
// ignore the case when the radius is so big that the circle fully covers the edge
// just draw a line for now, but maybe it can be replaced by drawing the arc
val c = sqrt(radius.pow(2) - circleCenterOffset.toDouble().pow(2))
if (c > center) {
shapePath.lineTo(length, 0f)
return
}
// draw a line from the left corner to the start of the arc
val arcStart = center - c
shapePath.lineTo(arcStart.toFloat(), 0f)
// calc the start angle and the sweep angle of the arc and draw the arc
// angles are measured clockwise with 0 degrees at 3 o'clock
val alpha = Math.toDegrees(asin(circleCenterOffset / radius).toDouble())
val startAngle = 180 + alpha
val sweepAngle = 180 - 2 * alpha
shapePath.addArc(
center - radius,
circleCenterOffset - radius,
center + radius,
circleCenterOffset + radius,
startAngle.toFloat(),
sweepAngle.toFloat())
// draw the line from the end of the arc to the right corner
shapePath.lineTo(length, 0f)
}
}
The method to construct a background drawable for banner:
fun createBannerBackgroundDrawable(backgroundColor: ColorStateList,
#Px circleRadius: Int,
#Px circleCenterOffset: Int,
#Px cornersRadius: Int,
#Px elevation: Int): Drawable {
val appearanceModel = ShapeAppearanceModel.builder()
.setTopEdge(BannerTopEdgeTreatment(circleRadius, circleCenterOffset))
.setAllCorners(CornerFamily.ROUNDED, cornersRadius.toFloat())
.build()
val drawable = MaterialShapeDrawable(appearanceModel)
drawable.fillColor = backgroundColor
drawable.elevation = elevation.toFloat()
return drawable
}
Then this drawable is used as a background of the banner view:
banner.background = createVerticalBannerBackground(...)
And also it is necessary to set clipChildren attribute of the parent view of the banner to false:
android:clipChildren="false"
The final result:

Android Kotlin: Translate animation on view is not working

I am working on an Android Kotlin project. I am applying animation on views. Starting from the basics, I am trying to animate an image view from the bottom of the screen to the center of the screen.
I have an XML layout with the following code.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:background="#color/colorPrimaryDark"
tools:context=".MainActivity">
<LinearLayout
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/main_image_logo"
android:src="#drawable/memento_text_logo"
android:layout_width="#dimen/main_logo_image_width"
android:layout_height="wrap_content" />
<TextView
android:textColor="#android:color/white"
android:id="#+id/main_tv_slogan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/main_slogan"
/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
I am animating the logo image translating from the bottom to the center (where it is originally) in the activity with the following code.
private fun animateMainLogo() {
val valueAnimator = ValueAnimator.ofFloat(0f, main_image_logo.y)
valueAnimator.addUpdateListener {
val value = it.animatedValue as Float
main_image_logo.translationY = value
}
valueAnimator.interpolator = LinearInterpolator()
valueAnimator.duration = 1000
valueAnimator.start()
}
When I run the code, it is not animating the view. It is just there where it is and static. What is wrong with my code and how can I fix it?
translationY of view in layout is 0. If you want to animate it from bottom to current position - you should change translationY values from some positive value to 0.
private fun animateLogo() {
val translationYFrom = 400f
val translationYTo = 0f
val valueAnimator = ValueAnimator.ofFloat(translationYFrom, translationYTo).apply {
interpolator = LinearInterpolator()
duration = 1000
}
valueAnimator.addUpdateListener {
val value = it.animatedValue as Float
main_image_logo?.translationY = value
}
valueAnimator.start()
}
Same thing can be done this way:
private fun animateLogo() {
main_image_logo.translationY = 400f
main_image_logo.animate()
.translationY(0f)
.setInterpolator(LinearInterpolator())
.setStartDelay(1000)
.start()
}
Add this lines to LinearLayout and ConstraintLayout because without them LinearLayout will cut of parts of animated view when it is outside of LinearLayout bounds.
android:clipChildren="false"
android:clipToPadding="false"
Or make main_image_logo direct child of root ConstraintLayout. Here is result:

How to properly set path values to describe an arc animation in Android

I have one view "whiteCircle" inside a button which I want to move from it's initial position to the white box "imageSquared" describing an arc and then get back to its initial position.
What I have tried is:
private fun startArcAnimation() {
val path = Path()
val location = IntArray(2)
imageSquared.getLocationOnScreen(location)
path.arcTo(
0f,
0f,
location[0].toFloat() + imageSquared.width,
location[0].toFloat() + imageSquared.height,
180f,
180f,
true
)
val animator = ObjectAnimator.ofFloat(whiteCircle, View.X, View.Y, path)
animator.duration = 1000
animator.start()
}
And this is the result:
Can you help me setting path values correct?
I have been struggling with arcTo properties without success.
Thanks in advance.
Here is my implementation:
arcTo method create oval which is placed into rect. So first of all you need to create correct rect. Your path should start from 180 degree in oval and move 180 degrees clockwise (Zero angle of oval is in right side).
I suggest to animate translationX and translationY properties of view.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
greenButton.setOnClickListener {
startArcAnimation()
}
}
private fun startArcAnimation() {
if (red.translationX != 0f) {
//return to start position animation
red.animate().translationX(0f).translationY(0f).start()
return
}
val rectHeight = 600
val left = 0f
val top = -rectHeight / 2
val right = white.x - red.x
val bottom = white.y + rectHeight / 2 - red.y
val path = Path()
path.arcTo(left, top.toFloat(), right, bottom, 180f, 180f, true)
val animator = ObjectAnimator.ofFloat(red, View.TRANSLATION_X, View.TRANSLATION_Y, path)
animator.duration = 1000
animator.start()
}
}
Here is activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000"
android:padding="16dp">
<Button
android:id="#+id/greenButton"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="#0a0"
android:stateListAnimator="#null" />
<ImageView
android:id="#+id/white"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center_vertical|right"
android:background="#fff" />
<ImageView
android:id="#+id/red"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="70dp"
android:layout_marginBottom="10dp"
android:background="#f00" />
</FrameLayout>

ObjectAnimator, Changing "ScaleX" or "Left," but text doesn't stay properly centered

I need to animate three elements to show additional info when the user clicks on a button. I have a label (TextView tvAvailableToPair) that slides out from under a button (Button bSearchForDevices)--both of which start with a width of fill_parent--while it simultaneously slides up. I have it changing perfectly, but it isn't animated: I've achieved that via SetLayoutParams:
bSearchForDevices.setText(R.string.bSearchForDevicesDiscoveryStartedText);
FrameLayout.LayoutParams lp_bSFD =
(FrameLayout.LayoutParams) bSearchForDevices.getLayoutParams();
int mWidthOfBSearchForDevices = lp_bSFD.width =
(int) Math.round(fMeasureScreenWidth * 0.4);
int mLeftMarginOfBSearchForDevices = lp_bSFD.leftMargin =
Math.round(fMeasureScreenWidth - (fHorizontalMargin + lp_bSFD.width));
bSearchForDevices.setLayoutParams(lp_bSFD);
lp_bSFD = (FrameLayout.LayoutParams) tvAvailableToPair.getLayoutParams();
lp_bSFD.width = mLeftMarginOfBSearchForDevices;
lp_bSFD.leftMargin = 0;
tvAvailableToPair.setLayoutParams(lp_bSFD);
ViewGroup.LayoutParams lp = llAvailableToPair.getLayoutParams();
lp.height = Math.round(getResources().getDimensionPixelSize(
R.dimen.llAvailableToPair_height_expanded) * MainActivity.displayMetrics.density);
I learned a FrameLayout allows you to place elements on different Z-axis values (on top of or below another element; to cover or be covered over by another element). Here's my xml for this activity:
<RelativeLayout
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"
tools:context=".MainActivity"
android:id="#+id/rlDeviceListLayout"
android:background="#android:color/holo_blue_dark"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true"
android:animateLayoutChanges="true"
app:layout_behavior="#string/appbar_scrolling_view_behavior" >
<android.support.design.widget.AppBarLayout
android:id="#+id/ablToolbarLayout"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_alignParentTop="true"
android:theme="#style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
app:popupTheme="#style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<LinearLayout>
<!-- Existing devices for user to select -->
</LinearLayout>
<!-- This is what needs to be animated up once user presses the button -->
<LinearLayout
android:id="#+id/llAvailableToPairLayout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="#dimen/llAvailableToPair_height_collapsed"
android:layout_alignParentBottom="true"
android:paddingTop="4dp">
<!-- This Layout is used to stack the button over the textview -->
<FrameLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginBottom="4dp">
<!-- We'll change the width of this programmatically so that it appears to
elegantly slide to the left -->
<TextView
android:id="#+id/tvAvailableToPair"
android:text="#string/tvAvailableToPair"
android:layout_width="fill_parent"
android:layout_height="#dimen/bSearchForDevices_height"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:inputType="none"
android:gravity="center"
android:foregroundGravity="left"
android:textSize="16sp"
android:textColor="#ffffff" />
<!-- We'll change the text, width and left of this so that it reveals the
textview under it. It needs to appear as if it were elegantly squeezed
toward the right -->
<Button
android:id="#+id/bSearchForDevices"
android:text="#string/bSearchForDevicesPreliminaryText"
android:layout_width="fill_parent"
android:layout_height="#dimen/bSearchForDevices_height"
android:layout_marginLeft="#dimen/activity_horizontal_margin"
android:layout_marginRight="#dimen/activity_horizontal_margin"
android:foregroundGravity="right"
android:visibility="visible"
android:theme="#style/Base.ThemeOverlay.AppCompat.Dark"
android:gravity="center" />
</FrameLayout>
<ListView
android:id="#+id/lvAvailableToPair"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:visibility="visible" />
</LinearLayout>
</RelativeLayout>
Its the last three elements I'm changing. Using SetLayoutParams keeps the text centered (and I shorten the text on the button to allow for smaller screens and the label -- I'll need to animate that as well next, possibly using TextSwitcher Animate Text Using TextSwitcher In Android) after the change. Moving on to getting those changes to animate, I had to use a different approach:
bSearchForDevices.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
FrameLayout.LayoutParams lpTVATP = (FrameLayout.LayoutParams) tvAvailableToPair.getLayoutParams();
Paint paint = new Paint();
AnimatorSet RevealAvailableDevices = new AnimatorSet();
float fMeasuretvAvailableToPairTextWidth = paint.measureText(tvAvailableToPair.getText().toString()); // Function returns dp, not pixels
float fMeasurebSearchForDevicesTextWidth = paint.measureText(bSearchForDevices.getText().toString());
float fMeasureScreenWidth = MainActivity.displayMetrics.widthPixels; // + 0.5f; // Extra 0.5 to simplify/speed up rounding w/o a lot of excess code (LoungeKatt, via https://stackoverflow.com/questions/1016896/get-screen-dimensions-in-pixels) and used on Google's Android docs for just this purpose # https://developer.android.com/guide/practices/screens_support.html#dips-pels
float fHorizontalMargin;
// If the screen is cramped, we'll shorten the margins:
if (fMeasuretvAvailableToPairTextWidth > (fMeasureScreenWidth * 0.5)) {
fHorizontalMargin = (getResources().getDimension(R.dimen.activity_horizontal_margin_compressed) / MainActivity.displayMetrics.density);
} else {
fHorizontalMargin = (getResources().getDimension(R.dimen.activity_horizontal_margin) / MainActivity.displayMetrics.density);
}
/* This works PERFECTLY, but isn't animated:
bSearchForDevices.setText(R.string.bSearchForDevicesDiscoveryStartedText);
FrameLayout.LayoutParams lp_bSFD = (FrameLayout.LayoutParams) bSearchForDevices.getLayoutParams();
int mWidthOfBSearchForDevices = lp_bSFD.width = (int) Math.round(fMeasureScreenWidth * 0.4);
int mLeftMarginOfBSearchForDevices = lp_bSFD.leftMargin = Math.round(fMeasureScreenWidth - (fHorizontalMargin + lp_bSFD.width));
bSearchForDevices.setLayoutParams(lp_bSFD);
lp_bSFD = (FrameLayout.LayoutParams) tvAvailableToPair.getLayoutParams();
lp_bSFD.width = mLeftMarginOfBSearchForDevices;
lp_bSFD.leftMargin = 0;
tvAvailableToPair.setLayoutParams(lp_bSFD);
ViewGroup.LayoutParams lp = llAvailableToPair.getLayoutParams();
lp.height = Math.round(getResources().getDimensionPixelSize(R.dimen.llAvailableToPair_height_expanded) * MainActivity.displayMetrics.density);
*/
float fWidthOfbSearchForDevices = (float) ((fMeasureScreenWidth * 0.4));
float fWidthOftvATP = (fMeasureScreenWidth - ((fWidthOfbSearchForDevices * 1) + (fHorizontalMargin * 2)));
float fXOfbSearchForDevices = (fMeasureScreenWidth - (fWidthOfbSearchForDevices + (1 * fHorizontalMargin)));
Log.d(TAG, "...\nfMeasureScreenWidth = " + fMeasureScreenWidth + "\nfWidthOfbSearchForDevices = " + fWidthOfbSearchForDevices + "\nfHorizontalMargin = " + fHorizontalMargin +
"\nbSearchForDevices.getX() = " + bSearchForDevices.getX() + "\n (fXOfbSearchForDevices) X is going to be " + fXOfbSearchForDevices);
ObjectAnimator aniButtonMove = ObjectAnimator.ofInt(bSearchForDevices, "Left", (int) fXOfbSearchForDevices);
bSearchForDevices.setPivotX(fXOfbSearchForDevices + (fWidthOfbSearchForDevices / 2)); // No effect: To be in the center of where the button will end up at.
ObjectAnimator aniTextViewResize = ObjectAnimator.ofInt(tvAvailableToPair, "Right", (int) fXOfbSearchForDevices);
Log.d(TAG, "tvAvailableToPair's width = " + tvAvailableToPair.getWidth() + " tvAvailableToPair is supposed to be " + fWidthOftvATP + "\nWidth of bSearchForDevices = " + bSearchForDevices.getWidth());
RevealAvailableDevices.play(aniTextViewResize).with(aniButtonMove);
RevealAvailableDevices.start();
This produces an animation that correctly moves the bounds of the textview and button, but doesn't center the text to the new bounds (edges/left and right coordinates) of those elements: my button now shows a lot of leading empty space just before it shows S, E and half of A of "Search For Devices;" in other words, it's clipped. I need it kept centered (as is when using SetLayoutParams). The same applies to the textview: the last 5 letters of "Available Devices" get clipped off.
Is there a better way to create this animation that has three elements changing shape (and preferably changing the button's text as well)? I've tried ScaleX but am unable to get it to sync correctly as it pivots at the center without sliding to the right side, nor the textview to slide while it shrinks...maybe ScaleX but change the pivot to the left edge... Or maybe a PropertyValuesHolder?
3/15/2016 Update:
Here's what I've got: https://www.dropbox.com/s/n14hed6xcknfh9b/SS_20160315_135938.mp4?dl=0
And what I need it to look like post animation

Categories

Resources