| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * 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.android.systemui.wallet.ui; |
| |
| import android.content.Context; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.util.MathUtils; |
| import android.view.View; |
| |
| import androidx.annotation.ColorInt; |
| import androidx.core.graphics.ColorUtils; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import com.android.systemui.R; |
| |
| final class DotIndicatorDecoration extends RecyclerView.ItemDecoration { |
| private final int mUnselectedRadius; |
| private final int mSelectedRadius; |
| private final int mDotMargin; |
| @ColorInt private final int mUnselectedColor; |
| @ColorInt private final int mSelectedColor; |
| private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| private WalletCardCarousel mCardCarousel; |
| |
| DotIndicatorDecoration(Context context) { |
| super(); |
| mUnselectedRadius = |
| context.getResources().getDimensionPixelSize( |
| R.dimen.card_carousel_dot_unselected_radius); |
| mSelectedRadius = |
| context.getResources().getDimensionPixelSize( |
| R.dimen.card_carousel_dot_selected_radius); |
| mDotMargin = context.getResources().getDimensionPixelSize(R.dimen.card_carousel_dot_margin); |
| |
| mUnselectedColor = context.getColor( |
| com.google.android.material.R.color.material_dynamic_neutral70); |
| mSelectedColor = context.getColor( |
| com.google.android.material.R.color.material_dynamic_neutral100); |
| } |
| |
| @Override |
| public void getItemOffsets( |
| Rect rect, View view, RecyclerView recyclerView, RecyclerView.State state) { |
| super.getItemOffsets(rect, view, recyclerView, state); |
| if (recyclerView.getAdapter().getItemCount() > 1) { |
| rect.bottom = |
| view.getResources().getDimensionPixelSize(R.dimen.card_carousel_dot_offset); |
| } |
| } |
| |
| @Override |
| public void onDrawOver(Canvas canvas, RecyclerView recyclerView, RecyclerView.State state) { |
| super.onDrawOver(canvas, recyclerView, state); |
| |
| mCardCarousel = (WalletCardCarousel) recyclerView; |
| int itemCount = recyclerView.getAdapter().getItemCount(); |
| if (itemCount <= 1) { |
| // Only shown if there are at least 2 items, and it's not a shimmer loader |
| return; |
| } |
| canvas.save(); |
| |
| float animationStartOffset = recyclerView.getWidth() / 6f; |
| // 0 when a card is still very prominent, ie. edgeToCenterDistance is greater than |
| // animationStartOffset |
| // 1 when the two cards are equidistant from the center ie. edgeToCenterDistance == 0 |
| float interpolatedProgress = |
| 1 - Math.min(Math.abs(mCardCarousel.mEdgeToCenterDistance), animationStartOffset) |
| / animationStartOffset; |
| |
| float totalWidth = |
| mDotMargin * (itemCount - 1) |
| + 2 * mUnselectedRadius * (itemCount - 2) |
| + 2 * mSelectedRadius; |
| // Translate the canvas so the drawing can always start at (0, 0) coordinates. |
| canvas.translate( |
| (recyclerView.getWidth() - totalWidth) / 2f, |
| recyclerView.getHeight() - mDotMargin); |
| |
| int itemsDrawn = 0; |
| while (itemsDrawn < itemCount) { |
| // count up from 0 to itemCount - 1 if LTR; count down from itemCount - 1 to 0 if RTL. |
| int i = isLayoutLtr() ? itemsDrawn : itemCount - itemsDrawn - 1; |
| |
| if (isSelectedItem(i)) { |
| drawSelectedDot(canvas, interpolatedProgress); |
| } else if (isNextItemInScrollingDirection(i)) { |
| drawFadingUnselectedDot(canvas, interpolatedProgress); |
| } else { |
| drawUnselectedDot(canvas); |
| } |
| canvas.translate(mDotMargin, 0); |
| itemsDrawn++; |
| } |
| |
| canvas.restore(); |
| this.mCardCarousel = null; // No need to hold a reference. |
| } |
| |
| private void drawSelectedDot(Canvas canvas, float progress) { |
| // Divide progress by 2 because the other half of the animation is done by |
| // drawFadingUnselectedDot. |
| mPaint.setColor( |
| getTransitionAdjustedColor( |
| ColorUtils.blendARGB(mSelectedColor, mUnselectedColor, progress / 2))); |
| float radius = MathUtils.lerp(mSelectedRadius, mUnselectedRadius, progress / 2); |
| canvas.drawCircle(radius, 0, radius, mPaint); |
| canvas.translate(radius * 2, 0); |
| } |
| |
| private void drawFadingUnselectedDot(Canvas canvas, float progress) { |
| // Divide progress by 2 because the first half of the animation is done by drawSelectedDot. |
| int blendedColor = |
| ColorUtils.blendARGB( |
| mUnselectedColor, mSelectedColor, progress / 2); |
| mPaint.setColor(getTransitionAdjustedColor(blendedColor)); |
| float radius = MathUtils.lerp(mUnselectedRadius, mSelectedColor, progress / 2); |
| canvas.drawCircle(radius, 0, radius, mPaint); |
| canvas.translate(radius * 2, 0); |
| } |
| |
| private void drawUnselectedDot(Canvas canvas) { |
| mPaint.setColor(mUnselectedColor); |
| canvas.drawCircle(mUnselectedRadius, 0, mUnselectedRadius, mPaint); |
| canvas.translate(mUnselectedRadius * 2, 0); |
| } |
| |
| private int getTransitionAdjustedColor(int color) { |
| int transitionAlphaOverride = 0xff; |
| return ColorUtils.setAlphaComponent(color, transitionAlphaOverride); |
| } |
| |
| private boolean isSelectedItem(int position) { |
| return mCardCarousel.mCenteredAdapterPosition == position; |
| } |
| |
| private boolean isNextItemInScrollingDirection(int position) { |
| if (isLayoutLtr()) { |
| return (mCardCarousel.mCenteredAdapterPosition + 1 == position |
| && mCardCarousel.mEdgeToCenterDistance >= 0f) |
| || (mCardCarousel.mCenteredAdapterPosition - 1 == position |
| && mCardCarousel.mEdgeToCenterDistance < 0f); |
| } |
| return (mCardCarousel.mCenteredAdapterPosition - 1 == position |
| && mCardCarousel.mEdgeToCenterDistance >= 0f) |
| || (mCardCarousel.mCenteredAdapterPosition + 1 == position |
| && mCardCarousel.mEdgeToCenterDistance < 0f); |
| } |
| |
| private boolean isLayoutLtr() { |
| if (mCardCarousel == null) { |
| // Shouldn't happen, but assume LTR for now. |
| return true; |
| } |
| return mCardCarousel.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; |
| } |
| } |