diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BuckOnboardingDialogView.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BuckOnboardingDialogView.kt new file mode 100644 index 000000000000..759bf237c06e --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BuckOnboardingDialogView.kt @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * 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.duckduckgo.app.onboarding.ui.page + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.util.AttributeSet +import android.view.ViewOutlineProvider +import android.view.animation.PathInterpolator +import android.widget.FrameLayout +import android.widget.ScrollView +import androidx.core.animation.doOnEnd +import androidx.core.content.ContextCompat +import androidx.core.view.isEmpty +import androidx.core.view.marginTop +import androidx.core.view.updateLayoutParams +import com.duckduckgo.app.browser.R +import com.duckduckgo.common.ui.view.toPx + +class BuckOnboardingDialogView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : FrameLayout(context, attrs, defStyle) { + + init { + alpha = 0f + background = ContextCompat.getDrawable(context, R.drawable.background_buck_onboarding_dialog) + clipToPadding = false + clipChildren = true + clipToOutline = true + outlineProvider = ViewOutlineProvider.BACKGROUND + } + + fun animateEntrance() { + if (isEmpty()) return + + val contentView = getChildAt(0) + contentView.alpha = 0f + + post { + val animationDuration = 800L + + val targetWidth = width + val targetHeight = height + val targetLayoutParamsHeight = layoutParams.height + val targetLayoutParamsWidth = layoutParams.width + val targetMarginTop = marginTop + + val initialWidth = 64.toPx() + val initialHeight = 64.toPx() + val initialContentTranslationY = 100f.toPx() + + // If the content view is a scrollable view, we want to disable the vertical scrollbar during animation + val verticalScrollbarEnabled = (contentView as? ScrollView)?.isVerticalScrollBarEnabled + + // https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#7e37d374-0c1b-4007-8187-6f29bb1fb3e7 + val standardEasingInterpolator = PathInterpolator(0.2f, 0f, 0f, 1f) + + // Start small (minimal dimensions) + updateLayoutParams { + width = initialWidth + height = initialHeight + + // Adjust the top margin to keep the bubble aligned to the bottom + (this as MarginLayoutParams).topMargin = targetMarginTop + (targetHeight - initialHeight) + } + + (contentView as? ScrollView)?.isVerticalScrollBarEnabled = false + + // Animate alpha + val alphaAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + duration = animationDuration / 2 + interpolator = standardEasingInterpolator + + addUpdateListener { animator -> + alpha = animator.animatedValue as Float + } + } + + // Animate width (horizontal expansion) + val widthAnimator = ValueAnimator.ofInt(initialWidth, targetWidth).apply { + duration = animationDuration / 2 + interpolator = standardEasingInterpolator + + addUpdateListener { animator -> + updateLayoutParams { + width = animator.animatedValue as Int + } + } + } + + // Animate height (vertical expansion from bottom to top) + val heightAnimator = ValueAnimator.ofInt(initialHeight, targetHeight).apply { + duration = animationDuration / 2 + startDelay = animationDuration / 2 // begin vertical expansion after horizontal expansion is complete + interpolator = standardEasingInterpolator + + addUpdateListener { animator -> + val currentHeight = animator.animatedValue as Int + updateLayoutParams { + height = currentHeight + (this as MarginLayoutParams).topMargin = targetMarginTop + (targetHeight - currentHeight) + } + } + } + + // Content fade-in + val contentAlphaAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + duration = animationDuration / 2 + startDelay = animationDuration / 2 + interpolator = standardEasingInterpolator + + addUpdateListener { animator -> + contentView.alpha = animator.animatedValue as Float + } + } + + // Content slide-up + val contentTranslationYAnimator = ValueAnimator.ofFloat(initialContentTranslationY, 0f).apply { + duration = animationDuration / 2 + startDelay = animationDuration / 2 + interpolator = standardEasingInterpolator + + addUpdateListener { animator -> + contentView.translationY = animator.animatedValue as Float + } + } + + AnimatorSet().apply { + playTogether( + alphaAnimator, + widthAnimator, + heightAnimator, + contentAlphaAnimator, + contentTranslationYAnimator, + ) + + doOnEnd { + if (verticalScrollbarEnabled != null) { + contentView.isVerticalScrollBarEnabled = verticalScrollbarEnabled + } + + updateLayoutParams { + width = targetLayoutParamsWidth + height = targetLayoutParamsHeight + } + } + + start() + } + } + } +} diff --git a/app/src/main/res/drawable/background_buck_onboarding_dialog.xml b/app/src/main/res/drawable/background_buck_onboarding_dialog.xml new file mode 100644 index 000000000000..0c6071fa8375 --- /dev/null +++ b/app/src/main/res/drawable/background_buck_onboarding_dialog.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/common/common-ui/src/main/res/values/design-system-theming.xml b/common/common-ui/src/main/res/values/design-system-theming.xml index 7ad6d7ea644b..0382a9ea8dda 100644 --- a/common/common-ui/src/main/res/values/design-system-theming.xml +++ b/common/common-ui/src/main/res/values/design-system-theming.xml @@ -245,6 +245,7 @@ @color/blue20 @color/purple20 @color/red10 + @color/daxOnboardingDialogBackgroundColorDark @@ -348,6 +349,7 @@ @color/blue50 @color/purple100 @color/red50 + @color/daxOnboardingDialogBackgroundColorLight diff --git a/common/common-ui/src/main/res/values/temp_colors.xml b/common/common-ui/src/main/res/values/temp_colors.xml index 14aa4f3942a0..63579d3283fd 100644 --- a/common/common-ui/src/main/res/values/temp_colors.xml +++ b/common/common-ui/src/main/res/values/temp_colors.xml @@ -24,6 +24,8 @@ + + #A591DC #6B4EBA @@ -33,4 +35,7 @@ #FFE080 #4284F4 + #FEFEFE + #011D34 + \ No newline at end of file