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"> + + + + + + + + - - - + + + + + + +