Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(370)

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/firstrun/ImageCarousel.java

Issue 954933004: Upstream Account chooser fragment of First Run. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Another attempt to suppress findbugs Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java/src/org/chromium/chrome/browser/firstrun/ImageCarousel.java
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/firstrun/ImageCarousel.java b/chrome/android/java/src/org/chromium/chrome/browser/firstrun/ImageCarousel.java
new file mode 100644
index 0000000000000000000000000000000000000000..1edd5122b547a8966c7163babb5dd140aa1e1810
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/firstrun/ImageCarousel.java
@@ -0,0 +1,410 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.firstrun;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.AttributeSet;
+import android.util.Property;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import org.chromium.chrome.R;
+
+import java.util.Arrays;
+
+/**
+ * Account chooser that displays profile images in a carousel and allows users to rotate it to
+ * select an account.
+ *
+ * Internally it is implemented using four ImageViews that get translated along the X axis based
+ * on the current carousel position.
+ *
+ * |'''''| |'''''| |'''''| |'''''| |'''''|
+ * |'''| |'''| |'''| |'''| |'''| |'''| |'''| |'''| |'''| |'''|
+ * |IM3| IM0 |IM1| -> |IM0| IM1 |IM2| -> |IM1| IM2 |IM3| -> |IM2| IM3 |IM0| -> |IM3| IM0 |IM1|
+ * |,,,| |,,,| |,,,| |,,,| |,,,| |,,,| |,,,| |,,,| |,,,| |,,,|
+ * |,,,,,| |,,,,,| |,,,,,| |,,,,,| |,,,,,|
+ *
+ * mPosition=0 mPosition=1 mPosition=2 mPosition=3 mPosition=4
+ *
+ * IM0 is mViews[0]
+ * IM1 is mViews[1]
+ * IM2 is mViews[2]
+ * IM3 is mViews[3]
+ *
+ * Each ImageView is displaying a profile image if there is one, however it is not necessarily true
+ * that IM0 is showing mImages[0] and IM1 is showing mImages[1], and so on. This changes when there
+ * are more than 4 accounts and ImageViews get reused for new accounts.
+ */
+public class ImageCarousel extends FrameLayout implements GestureDetector.OnGestureListener {
+
+ /**
+ * Constant used together image width to calculate how far should should each image move in
+ * x axis. This value was tweaked until images did not overlap with each other when scrolling.
+ */
+ private static final float TRANSLATION_FACTOR = 0.64f;
+
+ /**
+ * Constant used together with carousel width to calculate how should fling velocity in x axis
+ * be scaled when changing ImageCarousel position. It was tweaked for flings to look natural.
+ */
+ private static final float FLING_FACTOR = 20f * 0.92f / 2f;
+
+ /**
+ * Constant used together with carousel width to calculate how should scroll distance in x axis
+ * be scaled when changing ImageCarousel position. It was tweaked for image to follow user's
+ * finger when scrolling.
+ */
+ private static final float SCROLL_FACTOR = 0.92f / 2f;
+
+ /**
+ * Listener to ImageCarousel center position changes.
+ */
+ public interface ImageCarouselPositionChangeListener {
+ /**
+ * @param position The new center position of the ImageCarousel. It is a number in
+ * range [0, mImages.length).
+ */
+ void onPositionChanged(int position);
+ }
+
+ private static final int SCROLL_ANIMATION_DURATION_MS = 200;
+ private static final int ACCOUNT_SIGNED_IN_ANIMATION_DURATION_MS = 200;
+
+ /**
+ * Number of ImageViews used in ImageCarousel.
+ */
+ private static final int VIEW_COUNT = 4;
+
+ private static final int[] ORDER_OFFSETS = {2, 1, 3, 0};
+
+ private static final int[] POSITION_OFFSETS = {0, -1, 2, 1};
+
+ private static final int[] BITMAP_OFFSETS = {2, 1, -1, 0};
+
+ /**
+ * Property used to animate scrolling of the ImageCarousel.
+ */
+ private static final Property<ImageCarousel, Float> POSITION_PROPERTY =
+ new Property<ImageCarousel, Float>(Float.class, "") {
+ @Override
+ public Float get(ImageCarousel object) {
+ return object.mPosition;
+ }
+
+ @Override
+ public void set(ImageCarousel object, Float value) {
+ object.setPosition(value);
+ }
+ };
+
+ /**
+ * Property used to animate the alpha value of the images that are currently on the left and
+ * the right of the center image.
+ */
+ private static final Property<ImageCarousel, Float> BACKGROUND_IMAGE_ALPHA =
+ new Property<ImageCarousel, Float>(Float.class, "") {
+ @Override
+ public Float get(ImageCarousel object) {
+ return object.mViews[object.getChildDrawingOrder(VIEW_COUNT, 1)].getAlpha();
+ }
+
+ @Override
+ public void set(ImageCarousel object, Float value) {
+ object.mViews[object.getChildDrawingOrder(VIEW_COUNT, 1)].setAlpha(value);
+ object.mViews[object.getChildDrawingOrder(VIEW_COUNT, 2)].setAlpha(value);
+ }
+ };
+
+ /**
+ * Gesture detector used to capture scrolls, flings and taps on the image carousel.
+ */
+ private GestureDetector mGestureDetector;
+
+ /**
+ * Array that holds four ImageViews that are used to display images in the carousel.
+ */
+ private ImageView[] mViews = new ImageView[VIEW_COUNT];
+
+ /**
+ * Images that shown in the image carousel.
+ */
+ private Bitmap[] mImages;
+
+ private Animator mScrollAnimator;
+ private Animator mFadeInOutAnimator;
+
+ private float mPosition = 0f;
+
+ private ImageCarouselPositionChangeListener mListener;
+ private int mLastPosition = 0;
+ private boolean mNeedsPositionUpdates = true;
+
+ private int mCarouselWidth;
+ private int mImageWidth;
+ private float mScrollScalingFactor;
+ private float mFlingScalingFactor;
+ private float mTranslationFactor;
+
+ private boolean mScrollingDisabled;
+ private boolean mAccountSelected;
+
+ public ImageCarousel(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mGestureDetector = new GestureDetector(getContext(), this);
+ }
+
+ /**
+ * Scrolls ImageCarousel to the closest whole position for the desired position.
+ * @param position Desired ImageCarousel position.
+ * @param decelerate Whether animation should be decelerating.
+ * @param needsPositionUpdates Whether this scroll should trigger position update calls to
+ * mListener.
+ */
+ public void scrollTo(float position, boolean decelerate, boolean needsPositionUpdates) {
+ mNeedsPositionUpdates = needsPositionUpdates;
+ if (mScrollAnimator != null) mScrollAnimator.cancel();
+
+ position = Math.round(position);
+ mScrollAnimator = ObjectAnimator.ofFloat(this, POSITION_PROPERTY, mPosition, position);
+ mScrollAnimator.setDuration(SCROLL_ANIMATION_DURATION_MS);
+ if (decelerate) mScrollAnimator.setInterpolator(new DecelerateInterpolator());
+ mScrollAnimator.start();
+ }
+
+ /**
+ * @param listener Listener that should be notified on ImageCarousel center position changes.
+ */
+ public void setListener(ImageCarouselPositionChangeListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * @param images Images that should be displayed in the ImageCarousel.
+ */
+ public void setImages(Bitmap[] images) {
+ switch (images.length) {
+ case 0:
+ mImages = null;
+ mScrollingDisabled = true;
+ break;
+ case 1:
+ mScrollingDisabled = true;
+ mImages = Arrays.copyOf(images, images.length);
+ break;
+ default:
+ // Enable scrolling only if no account has already been selected.
+ mScrollingDisabled = mAccountSelected;
+ mImages = Arrays.copyOf(images, images.length);
+ break;
+ }
+
+ updateImageViews();
+ }
+
+ /**
+ * Sets the ImageCarousel to signed in mode that disables scrolling, animates away the
+ * background images, and displays a checkmark next to the account image that was chosen.
+ */
+ public void setSignedInMode() {
+ mScrollingDisabled = true;
+ mAccountSelected = true;
+ setPosition(getCenterPosition());
+
+ ImageView checkmark = new ImageView(getContext());
+ checkmark.setImageResource(R.drawable.verify_checkmark);
+ setLayoutParamsForCheckmark(checkmark);
+ addView(checkmark);
+
+ if (mFadeInOutAnimator != null) mFadeInOutAnimator.cancel();
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(
+ ObjectAnimator.ofFloat(this, BACKGROUND_IMAGE_ALPHA, 0),
+ ObjectAnimator.ofFloat(checkmark, View.ALPHA, 0.0f, 1.0f));
+ mFadeInOutAnimator = animatorSet;
+ mFadeInOutAnimator.setDuration(ACCOUNT_SIGNED_IN_ANIMATION_DURATION_MS);
+ mFadeInOutAnimator.start();
+ }
+
+ @Override
+ public void onFinishInflate() {
+ super.onFinishInflate();
+
+ mImageWidth = getResources().getDimensionPixelSize(R.dimen.fre_image_carousel_height);
+ for (int i = 0; i < VIEW_COUNT; ++i) {
+ ImageView view = new ImageView(getContext());
+ FrameLayout.LayoutParams params =
+ new FrameLayout.LayoutParams(mImageWidth, mImageWidth);
+ params.gravity = Gravity.CENTER;
+ view.setLayoutParams(params);
+ mViews[i] = view;
+ addView(view);
+ }
+
+ mCarouselWidth = getResources().getDimensionPixelSize(R.dimen.fre_image_carousel_width);
+ mScrollScalingFactor = SCROLL_FACTOR * mCarouselWidth;
+ mFlingScalingFactor = FLING_FACTOR * mCarouselWidth;
+ mTranslationFactor = TRANSLATION_FACTOR * mImageWidth;
+
+ setChildrenDrawingOrderEnabled(true);
+ setPosition(0f);
+ }
+
+ /**
+ * @return The index of the view that should be drawn on the given iteration.
+ */
+ @Override
+ protected int getChildDrawingOrder(int childCount, int iteration) {
+ // Draw the views that are not our 4 ImagesViews in their normal order.
+ if (iteration >= VIEW_COUNT) return iteration;
+
+ // Draw image views in the correct z order based on the current position.
+ return (Math.round(mPosition) + ORDER_OFFSETS[iteration]) % VIEW_COUNT;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mScrollingDisabled) return false;
+ if (mGestureDetector.onTouchEvent(event)) return true;
+
+ if (event.getAction() == MotionEvent.ACTION_UP
+ || event.getAction() == MotionEvent.ACTION_CANCEL) {
+ scrollTo(mPosition, false, true);
+ }
+
+ return false;
+ }
+
+ // Implementation of GestureDetector.OnGestureListener
+
+ @Override
+ public boolean onDown(MotionEvent motionEvent) {
+ return true;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent motionEvent) {}
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent motionEvent) {
+ mNeedsPositionUpdates = true;
+ if (motionEvent.getX() < (mCarouselWidth - mImageWidth) / 2f) {
+ scrollTo(mPosition - 1, false, true);
+ return true;
+ } else if (motionEvent.getX() > (mCarouselWidth + mImageWidth) / 2f) {
+ scrollTo(mPosition + 1, false, true);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ // Once the user has started scrolling, prevent the parent view from handling touch events.
+ // This allows the ImageCarousel to be behave reasonably when nested inside a ScrollView.
+ getParent().requestDisallowInterceptTouchEvent(true);
+
+ mNeedsPositionUpdates = true;
+ setPosition(mPosition + distanceX / mScrollScalingFactor);
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent motionEvent) {}
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ mNeedsPositionUpdates = true;
+ scrollTo(mPosition - velocityX / mFlingScalingFactor, true, true);
+ return true;
+ }
+
+ // Internal methods
+
+ /**
+ * Updates the position, scale, alpha and image shown for all four ImageViews used by
+ * the ImageCarousel.
+ */
+ private void updateImageViews() {
+ if (mImages == null) return;
+
+ for (int i = 0; i < VIEW_COUNT; i++) {
+ if (mAccountSelected && i != getCenterPosition()) continue;
+
+ ImageView image = mViews[i];
+
+ updateBitmap(i);
+
+ final float position = mPosition + POSITION_OFFSETS[i];
+
+ // X translation is a sin function with a period of 4 and with range
+ // [-mTranslationFactor, mTranslationFactor]
+ image.setTranslationX(
+ -mTranslationFactor * ((float) Math.sin(position * Math.PI / 2f)));
+
+ // scale is a cos function with a period of 4 and range [1/3, 1]
+ // scale is 1 when the image is in the front and 1/3 when the image is behind other
+ // images.
+ final float scale = (float) Math.cos(position * Math.PI / 2f) / 3f + 2f / 3f;
+ image.setScaleY(scale);
+ image.setScaleX(scale);
+
+ // alpha is a cos^2 function with a period of 2 and range [0, 1]
+ // alpha is 1 when the image is in the center in the front and 0 when it is in the back.
+ final float alpha = (float) Math.pow(Math.cos(position * Math.PI / 4f), 2);
+ image.setAlpha(alpha);
+ }
+ }
+
+ private void updateBitmap(int i) {
+ if (mImages.length == 1 && i < 3) return;
+ ImageView image = mViews[getChildDrawingOrder(VIEW_COUNT, i)];
+ image.setImageBitmap(mImages[
+ (mImages.length + Math.round(mPosition) + BITMAP_OFFSETS[i]) % mImages.length]);
+ }
+
+ private void setPosition(float position) {
+ if (mImages != null) {
+ mPosition = ((position % mImages.length) + mImages.length) % mImages.length;
+ }
+
+ int adjustedPosition = getCenterPosition();
+ if (adjustedPosition != mLastPosition) {
+ mLastPosition = adjustedPosition;
+ if (mListener != null && mNeedsPositionUpdates) {
+ mListener.onPositionChanged(adjustedPosition);
+ }
+ }
+
+ // Need to call invalidate() for getChildDrawingOrder() to be called since the image
+ // order has changed.
+ updateImageViews();
+ invalidate();
+ }
+
+ private int getCenterPosition() {
+ if (mImages == null) return 0;
+ return Math.round(mPosition) % mImages.length;
+ }
+
+ private void setLayoutParamsForCheckmark(View view) {
+ int size = getResources().getDimensionPixelSize(R.dimen.fre_checkmark_size);
+ FrameLayout.LayoutParams params =
+ new FrameLayout.LayoutParams(size, size);
+ params.gravity = Gravity.CENTER;
+ view.setLayoutParams(params);
+ view.setTranslationX((mImageWidth - size) / 2f);
+ view.setTranslationY((mImageWidth - size) / 2f);
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698