From 79beb2b6ea5f96468c62ef2df81e15df9c154cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A4my=20Zehnder?= Date: Thu, 7 Nov 2019 18:27:39 +0100 Subject: [PATCH] Enlarge user profile image when it is tapped Profile pictures are bigger than the space available in the user fragment. That sometimes cuts off faces of people. Shows the full image upon tapping on the user profile image. --- .../bitrite/android/ws/ui/UserFragment.java | 175 ++++- app/src/main/res/layout/fragment_user.xml | 684 +++++++++--------- 2 files changed, 526 insertions(+), 333 deletions(-) diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/UserFragment.java b/app/src/main/java/fi/bitrite/android/ws/ui/UserFragment.java index 28660102..99c7a8a9 100644 --- a/app/src/main/java/fi/bitrite/android/ws/ui/UserFragment.java +++ b/app/src/main/java/fi/bitrite/android/ws/ui/UserFragment.java @@ -1,7 +1,14 @@ package fi.bitrite.android.ws.ui; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Intent; import android.graphics.Color; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.text.Html; @@ -15,6 +22,7 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ImageView; @@ -22,7 +30,10 @@ import android.widget.TextView; import android.widget.Toast; -import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.Target; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -38,9 +49,11 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import butterknife.BindColor; +import butterknife.BindInt; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnCheckedChanged; +import butterknife.OnClick; import fi.bitrite.android.ws.R; import fi.bitrite.android.ws.api.helper.HttpErrorHelper; import fi.bitrite.android.ws.model.Feedback; @@ -76,6 +89,7 @@ public class UserFragment extends BaseFragment { @BindView(R.id.user_swipe_refresh) SwipeRefreshLayout mSwipeRefresh; @BindView(R.id.user_layout_details) LinearLayout mLayoutDetails; @BindView(R.id.user_img_photo) ImageView mImgPhoto; + @BindView(R.id.user_img_expanded) ImageView mImgExpanded; @BindView(R.id.user_lbl_name) TextView mLblName; @BindView(R.id.user_img_favorite) ImageView mImgFavorite; @@ -101,6 +115,8 @@ public class UserFragment extends BaseFragment { @BindColor(R.color.rating_neutral) int mRatingColorNeutral; @BindColor(R.color.rating_negative) int mRatingColorNegative; + @BindInt(android.R.integer.config_shortAnimTime) int mShortAnimationDuration; + private BehaviorSubject mLastUserInfoLoadResult = BehaviorSubject.create(); private int mUserId; @@ -109,6 +125,9 @@ public class UserFragment extends BaseFragment { private final BehaviorSubject> mFeedbacks = BehaviorSubject.create(); private final BehaviorSubject mFavorite = BehaviorSubject.create(); + private boolean mIsImgExpandible; + private Animator mCurrentAnimator; + private boolean mDbFavoriteStatus; private FeedbackListAdapter mFeedbackListAdapter; @@ -319,12 +338,34 @@ private void updateUserViewContent() { } // If we're connected and there is a picture, get user picture. + mIsImgExpandible = false; + mImgPhoto.setImageResource(R.drawable.default_userinfo_profile); String url = user.profilePicture.getLargeUrl(); if (!TextUtils.isEmpty(url)) { WSGlide.with(requireContext()) .load(url) - .apply(new RequestOptions().placeholder(R.drawable.default_userinfo_profile)) - .into(mImgPhoto); + .addListener(new RequestListener() { + @Override + public boolean onResourceReady(Drawable resource, + Object model, + Target target, + DataSource dataSource, + boolean isFirstResource) { + mIsImgExpandible = true; + mImgPhoto.setImageDrawable(resource); + mImgExpanded.setImageDrawable(resource); + return false; + } + + @Override + public boolean onLoadFailed(@Nullable GlideException e, + Object model, + Target target, + boolean isFirstResource) { + return false; + } + }) + .preload(); mImgPhoto.setContentDescription( getString(R.string.content_description_avatar_of_var, user.getName())); } @@ -403,6 +444,134 @@ private void loadUserInformation() { .subscribe(); } + @OnClick(R.id.user_img_photo) + void zoomImageFromThumb() { + if (!mIsImgExpandible) { + return; + } + + // If there's an animation in progress, cancel it + // immediately and proceed with this one. + if (mCurrentAnimator != null) { + mCurrentAnimator.cancel(); + } + + // Load the high-resolution "zoomed-in" image. + mImgExpanded.setImageDrawable(mImgPhoto.getDrawable()); + + // Calculate the starting and ending bounds for the zoomed-in image. + // This step involves lots of math. Yay, math. + final Rect startBounds = new Rect(); + final Rect finalBounds = new Rect(); + final Point globalOffset = new Point(); + + // The start bounds are the global visible rectangle of the thumbnail, + // and the final bounds are the global visible rectangle of the container + // view. Also set the container view's offset as the origin for the + // bounds, since that's the origin for the positioning animation + // properties (X, Y). + mImgPhoto.getGlobalVisibleRect(startBounds); + mSwipeRefresh.getGlobalVisibleRect(finalBounds, globalOffset); + startBounds.offset(-globalOffset.x, -globalOffset.y); + finalBounds.offset(-globalOffset.x, -globalOffset.y); + + // Adjust the start bounds to be the same aspect ratio as the final + // bounds using the "center crop" technique. This prevents undesirable + // stretching during the animation. Also calculate the start scaling + // factor (the end scaling factor is always 1.0). + float startScale; + if ((float) finalBounds.width() / finalBounds.height() + > (float) startBounds.width() / startBounds.height()) { + // Extend start bounds horizontally + startScale = (float) startBounds.height() / finalBounds.height(); + float startWidth = startScale * finalBounds.width(); + float deltaWidth = (startWidth - startBounds.width()) / 2; + startBounds.left -= deltaWidth; + startBounds.right += deltaWidth; + } else { + // Extend start bounds vertically + startScale = (float) startBounds.width() / finalBounds.width(); + float startHeight = startScale * finalBounds.height(); + float deltaHeight = (startHeight - startBounds.height()) / 2; + startBounds.top -= deltaHeight; + startBounds.bottom += deltaHeight; + } + + // Hide the thumbnail and show the zoomed-in view. When the animation + // begins, it will position the zoomed-in view in the place of the + // thumbnail. + mImgPhoto.setAlpha(0.3f); + mImgExpanded.setVisibility(View.VISIBLE); + + // Set the pivot point for SCALE_X and SCALE_Y transformations + // to the top-left corner of the zoomed-in view (the default + // is the center of the view). + mImgExpanded.setPivotX(0f); + mImgExpanded.setPivotY(0f); + + // Construct and run the parallel animation of the four translation and + // scale properties (X, Y, SCALE_X, and SCALE_Y). + AnimatorSet set = new AnimatorSet(); + set + .play(ObjectAnimator.ofFloat(mImgExpanded, View.X, startBounds.left, finalBounds.left)) + .with(ObjectAnimator.ofFloat(mImgExpanded, View.Y, startBounds.top, finalBounds.top)) + .with(ObjectAnimator.ofFloat(mImgExpanded, View.SCALE_X, startScale, 1f)) + .with(ObjectAnimator.ofFloat(mImgExpanded, View.SCALE_Y, startScale, 1f)); + set.setDuration(mShortAnimationDuration); + set.setInterpolator(new DecelerateInterpolator()); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCurrentAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + mCurrentAnimator = null; + } + }); + set.start(); + mCurrentAnimator = set; + + // Upon clicking the zoomed-in image, it should zoom back down + // to the original bounds and show the thumbnail instead of + // the expanded image. + final float startScaleFinal = startScale; + mImgExpanded.setOnClickListener(view -> { + if (mCurrentAnimator != null) { + mCurrentAnimator.cancel(); + } + + // Animate the four positioning/sizing properties in parallel, + // back to their original values. + AnimatorSet set1 = new AnimatorSet(); + set1 + .play(ObjectAnimator.ofFloat(mImgExpanded, View.X, startBounds.left)) + .with(ObjectAnimator.ofFloat(mImgExpanded, View.Y,startBounds.top)) + .with(ObjectAnimator.ofFloat(mImgExpanded, View.SCALE_X, startScaleFinal)) + .with(ObjectAnimator.ofFloat(mImgExpanded, View.SCALE_Y, startScaleFinal)); + set1.setDuration(mShortAnimationDuration); + set1.setInterpolator(new DecelerateInterpolator()); + set1.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mImgPhoto.setAlpha(1f); + mImgExpanded.setVisibility(View.GONE); + mCurrentAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + mImgPhoto.setAlpha(1f); + mImgExpanded.setVisibility(View.GONE); + mCurrentAnimator = null; + } + }); + set1.start(); + mCurrentAnimator = set1; + }); + } + private void reloadUserInformation() { mUserRepository.markAsOld(mUserId); mFeedbackRepository.markAsOldForRecipient(mUserId); diff --git a/app/src/main/res/layout/fragment_user.xml b/app/src/main/res/layout/fragment_user.xml index 29aef145..307ddfee 100644 --- a/app/src/main/res/layout/fragment_user.xml +++ b/app/src/main/res/layout/fragment_user.xml @@ -1,400 +1,424 @@ - - + - - - - - + android:layout_height="fill_parent" + android:background="@color/primaryWhite" + android:scrollbars="vertical"> - + + + android:layout_height="220dp" + android:background="#333333"> + + - + android:layout_height="match_parent" + android:gravity="bottom" + android:orientation="vertical" + android:paddingBottom="10dp"> - - + - + + - - + + + + + + + + + + + + + + + + android:layout_height="1dp" + android:layout_marginBottom="8dp" + android:layout_marginStart="72dp" + android:layout_marginLeft="72dp" + android:layout_marginRight="0dp" + android:layout_marginTop="8dp" + android:alpha="0.2" + android:background="@color/primaryTextColorLight" /> - + + + + + + + + + tools:text="Member since 2005, Last login January 1, 2015" /> - - - - - - + android:layout_marginStart="56dp" + android:layout_marginLeft="56dp" + tools:text="Max guests: 3, Languages: English, Spanish" /> - + - - + android:layout_marginStart="16dp" + android:layout_marginLeft="16dp" + android:orientation="vertical" + android:paddingBottom="8dp" + android:paddingTop="8dp"> + + + + + + + + - - - - - - + android:layout_marginStart="56dp" + android:layout_marginLeft="56dp" + android:visibility="gone" /> - + - + + android:layout_marginStart="16dp" + android:layout_marginLeft="16dp" + android:orientation="vertical" + android:paddingTop="8dp"> - + + + + + + - - - - - - + android:layout_marginStart="56dp" + android:layout_marginLeft="56dp" + android:autoLink="all" + tools:maxLines="5" + tools:text="@tools:sample/lorem/random" /> - - + - + android:layout_height="1dp" + android:layout_marginBottom="8dp" + android:layout_marginStart="72dp" + android:layout_marginLeft="72dp" + android:layout_marginEnd="0dp" + android:layout_marginRight="0dp" + android:layout_marginTop="8dp" + android:alpha="0.2" + android:background="@color/primaryTextColorLight" /> - + + + + + + + + + - - - - - - - - - - - - - - + android:layout_marginStart="56dp" + android:layout_marginLeft="56dp" + tools:text="Services this user may offer: " /> + android:layout_marginStart="56dp" + android:layout_marginLeft="56dp" + tools:text="Nearby services" /> - - - - - - - - - + - - - - + + - - - + android:layout_gravity="center_horizontal" + android:orientation="horizontal"> + + + + + + + + - - - + + + + + + +