diff --git a/app/src/main/java/org/wikipedia/bridge/JavaScriptActionHandler.kt b/app/src/main/java/org/wikipedia/bridge/JavaScriptActionHandler.kt index a6db649acd6..e5cb757b7b4 100644 --- a/app/src/main/java/org/wikipedia/bridge/JavaScriptActionHandler.kt +++ b/app/src/main/java/org/wikipedia/bridge/JavaScriptActionHandler.kt @@ -145,4 +145,21 @@ object JavaScriptActionHandler { "document.head.appendChild(style);" + "})();" } + + @JvmStatic + fun getElementAtPosition(x: Int, y: Int): String { + return "(function() {" + + " let element = document.elementFromPoint(${x}, ${y});" + + " let result = {};" + + " result.left = element.getBoundingClientRect().left;" + + " result.top = element.getBoundingClientRect().top;" + + " result.width = element.clientWidth;" + + " result.height = element.clientHeight;" + + " result.src = element.src;" + + " return result;" + + "})();" + } + + data class ImageHitInfo(val left: Float = 0f, val top: Float = 0f, val width: Float = 0f, val height: Float = 0f, + val src: String = "", val centerCrop: Boolean = false) } diff --git a/app/src/main/java/org/wikipedia/gallery/GalleryActivity.java b/app/src/main/java/org/wikipedia/gallery/GalleryActivity.java index ac50daf3e9d..fcd2d6e73b2 100644 --- a/app/src/main/java/org/wikipedia/gallery/GalleryActivity.java +++ b/app/src/main/java/org/wikipedia/gallery/GalleryActivity.java @@ -15,6 +15,7 @@ import android.view.Menu; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; @@ -37,6 +38,7 @@ import org.wikipedia.activity.BaseActivity; import org.wikipedia.analytics.GalleryFunnel; import org.wikipedia.auth.AccountUtil; +import org.wikipedia.bridge.JavaScriptActionHandler; import org.wikipedia.commons.FilePageActivity; import org.wikipedia.commons.ImageTagsProvider; import org.wikipedia.dataclient.Service; @@ -59,6 +61,7 @@ import org.wikipedia.theme.Theme; import org.wikipedia.util.ClipboardUtil; import org.wikipedia.util.DeviceUtil; +import org.wikipedia.util.DimenUtil; import org.wikipedia.util.FeedbackUtil; import org.wikipedia.util.GradientUtil; import org.wikipedia.util.ImageUrlUtil; @@ -67,6 +70,7 @@ import org.wikipedia.util.log.L; import org.wikipedia.views.PositionAwareFragmentStateAdapter; import org.wikipedia.views.ViewAnimations; +import org.wikipedia.views.ViewUtil; import org.wikipedia.views.WikiErrorView; import java.io.File; @@ -111,10 +115,13 @@ public class GalleryActivity extends BaseActivity implements LinkPreviewDialog.C public static final String EXTRA_FEATURED_IMAGE = "featuredImage"; public static final String EXTRA_FEATURED_IMAGE_AGE = "featuredImageAge"; + private static JavaScriptActionHandler.ImageHitInfo TRANSITION_INFO; + @NonNull private WikipediaApp app = WikipediaApp.getInstance(); @NonNull private ExclusiveBottomSheetPresenter bottomSheetPresenter = new ExclusiveBottomSheetPresenter(); @Nullable private PageTitle pageTitle; + @BindView(R.id.gallery_transition_receiver) ImageView transitionReceiver; @BindView(R.id.gallery_toolbar_container) ViewGroup toolbarContainer; @BindView(R.id.gallery_toolbar) Toolbar toolbar; @BindView(R.id.gallery_toolbar_gradient) View toolbarGradient; @@ -255,7 +262,33 @@ public void onCreate(Bundle savedInstanceState) { } setControlsShowing(controlsShowing); }); - loadGalleryContent(); + + if (TRANSITION_INFO != null + && TRANSITION_INFO.getWidth() > 0 && TRANSITION_INFO.getHeight() > 0) { + + float aspect = TRANSITION_INFO.getHeight() / TRANSITION_INFO.getWidth(); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(DimenUtil.getDisplayWidthPx(), (int)(DimenUtil.getDisplayWidthPx() * aspect)); + params.gravity = Gravity.CENTER_VERTICAL; + transitionReceiver.setLayoutParams(params); + + transitionReceiver.setVisibility(View.VISIBLE); + ViewUtil.loadImage(transitionReceiver, TRANSITION_INFO.getSrc(), TRANSITION_INFO.getCenterCrop(), false); + + final int transitionMillis = 500; + transitionReceiver.postDelayed(() -> { + if (isDestroyed()) { + return; + } + loadGalleryContent(); + }, transitionMillis); + + } else { + + TRANSITION_INFO = null; + transitionReceiver.setVisibility(View.GONE); + loadGalleryContent(); + + } } @Override public void onDestroy() { @@ -268,6 +301,7 @@ public void onCreate(Bundle savedInstanceState) { unbinder.unbind(); } + TRANSITION_INFO = null; super.onDestroy(); } @@ -358,6 +392,10 @@ public void onActivityResult(int requestCode, int resultCode, final Intent data) startCaptionEdit(item); } + public static void setTransitionInfo(@NonNull JavaScriptActionHandler.ImageHitInfo hitInfo) { + TRANSITION_INFO = hitInfo; + } + private void startCaptionEdit(GalleryItemFragment item) { PageTitle title = new PageTitle(item.getImageTitle().getPrefixedText(), new WikiSite(Service.COMMONS_URL, sourceWiki.languageCode())); String currentCaption = item.getMediaInfo().getCaptions().get(sourceWiki.languageCode()); @@ -448,6 +486,11 @@ public void onPageSelected(int position) { } currentPosition = position; } + + @Override + public void onPageScrollStateChanged(int state) { + hideTransitionReceiver(false); + } } @Override @@ -470,9 +513,40 @@ public void onBackPressed() { if (item != null && item.getImageTitle() != null && funnel != null) { funnel.logGalleryClose(pageTitle, item.getImageTitle().getDisplayText()); } + + if (TRANSITION_INFO != null) { + showTransitionReceiver(); + } + super.onBackPressed(); } + + public void onMediaLoaded() { + hideTransitionReceiver(true); + } + + private void showTransitionReceiver() { + transitionReceiver.setVisibility(View.VISIBLE); + } + + private void hideTransitionReceiver(boolean delay) { + if (transitionReceiver.getVisibility() == View.GONE) { + return; + } + if (delay) { + final int hideDelayMillis = 250; + transitionReceiver.postDelayed(() -> { + if (isDestroyed() || transitionReceiver == null) { + return; + } + transitionReceiver.setVisibility(View.GONE); + }, hideDelayMillis); + } else { + transitionReceiver.setVisibility(View.GONE); + } + } + /** * Show or hide all the UI controls in this activity (slide them out or in). * @param showing Whether to show or hide the controls. @@ -581,7 +655,6 @@ private void fetchGalleryItems() { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mediaList -> { - updateProgressBar(false); applyGalleryList(mediaList.getItems("image", "video")); }, caught -> { updateProgressBar(false); @@ -598,7 +671,6 @@ private void loadGalleryContent() { FeaturedImage featuredImage = GsonUnmarshaller.unmarshal(FeaturedImage.class, getIntent().getStringExtra(EXTRA_FEATURED_IMAGE)); featuredImage.setAge(getIntent().getIntExtra(EXTRA_FEATURED_IMAGE_AGE, 0)); - updateProgressBar(false); applyGalleryList(Collections.singletonList(new MediaListItem(featuredImage.title()))); } else { fetchGalleryItems(); diff --git a/app/src/main/java/org/wikipedia/gallery/GalleryItemFragment.java b/app/src/main/java/org/wikipedia/gallery/GalleryItemFragment.java index 5299bc9cbc1..8c161bcf47d 100644 --- a/app/src/main/java/org/wikipedia/gallery/GalleryItemFragment.java +++ b/app/src/main/java/org/wikipedia/gallery/GalleryItemFragment.java @@ -1,6 +1,7 @@ package org.wikipedia.gallery; import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; @@ -18,6 +19,10 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +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 com.github.chrisbanes.photoview.PhotoView; import org.wikipedia.Constants; @@ -54,7 +59,7 @@ import static org.wikipedia.util.PermissionUtil.hasWriteExternalStoragePermission; import static org.wikipedia.util.PermissionUtil.requestWriteStorageRuntimePermissions; -public class GalleryItemFragment extends Fragment { +public class GalleryItemFragment extends Fragment implements RequestListener { private static final String ARG_PAGETITLE = "pageTitle"; private static final String ARG_GALLERY_ITEM = "galleryItem"; @@ -309,14 +314,27 @@ private void loadVideo() { } private void loadImage(String url) { - imageView.setVisibility(View.VISIBLE); + imageView.setVisibility(View.INVISIBLE); L.v("Loading image from url: " + url); updateProgressBar(true); - ViewUtil.loadImageWithWhiteBackground(imageView, url); + ViewUtil.loadImageWithWhiteBackground(imageView, url, this); // TODO: show error if loading failed. } + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + ((GalleryActivity) requireActivity()).onMediaLoaded(); + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + imageView.setVisibility(View.VISIBLE); + ((GalleryActivity) requireActivity()).onMediaLoaded(); + return false; + } + private void shareImage() { if (getMediaInfo() == null) { return; diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.java b/app/src/main/java/org/wikipedia/page/PageFragment.java index 4e0c6b250a3..2728c5b98d0 100755 --- a/app/src/main/java/org/wikipedia/page/PageFragment.java +++ b/app/src/main/java/org/wikipedia/page/PageFragment.java @@ -19,6 +19,7 @@ import android.webkit.WebResourceRequest; import android.webkit.WebResourceResponse; import android.webkit.WebView; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -27,6 +28,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.content.res.AppCompatResources; import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.app.ActivityOptionsCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; @@ -100,6 +102,7 @@ import org.wikipedia.util.log.L; import org.wikipedia.views.ObservableWebView; import org.wikipedia.views.SwipeRefreshLayoutWithScroll; +import org.wikipedia.views.ViewUtil; import org.wikipedia.views.WikiErrorView; import org.wikipedia.wiktionary.WiktionaryDialog; @@ -183,6 +186,7 @@ public interface Callback { private ShareHandler shareHandler; private CompositeDisposable disposables = new CompositeDisposable(); private ActiveTimer activeTimer = new ActiveTimer(); + private ImageView viewForTransition; private PageReferences references; private long revision; @Nullable private AvPlayer avPlayer; @@ -621,6 +625,11 @@ public void onResume() { super.onResume(); initPageScrollFunnel(); activeTimer.resume(); + + if (viewForTransition != null) { + ((ViewGroup) viewForTransition.getParent()).removeView(viewForTransition); + viewForTransition = null; + } } @Override @@ -1165,9 +1174,39 @@ public void startDescriptionEditActivity(@Nullable String text) { private void startGalleryActivity(@NonNull String fileName) { if (app.isOnline()) { - requireActivity().startActivityForResult(GalleryActivity.newIntent(requireActivity(), - model.getTitle(), fileName, - model.getTitle().getWikiSite(), getRevision(), GalleryFunnel.SOURCE_NON_LEAD_IMAGE), ACTIVITY_REQUEST_GALLERY); + + bridge.evaluate(JavaScriptActionHandler.getElementAtPosition(DimenUtil.roundedPxToDp(webView.getLastTouchX()), DimenUtil.roundedPxToDp(webView.getLastTouchY())), s -> { + if (!isAdded()) { + return; + } + JavaScriptActionHandler.ImageHitInfo hitInfo = GsonUtil.getDefaultGson().fromJson(s, JavaScriptActionHandler.ImageHitInfo.class); + + viewForTransition = new ImageView(requireActivity()); + ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(DimenUtil.roundedDpToPx(hitInfo.getWidth()), DimenUtil.roundedDpToPx(hitInfo.getHeight())); + params.topMargin = DimenUtil.roundedDpToPx(hitInfo.getTop()); + params.leftMargin = DimenUtil.roundedDpToPx(hitInfo.getLeft()); + viewForTransition.setLayoutParams(params); + viewForTransition.setTransitionName(getString(R.string.transition_page_gallery)); + ((ViewGroup) webView.getParent()).addView(viewForTransition); + + GalleryActivity.setTransitionInfo(hitInfo); + + viewForTransition.post(() -> { + if (!isAdded()) { + return; + } + + ActivityOptionsCompat options = ActivityOptionsCompat. + makeSceneTransitionAnimation(requireActivity(), viewForTransition, getString(R.string.transition_page_gallery)); + + requireActivity().startActivityForResult(GalleryActivity.newIntent(requireActivity(), + model.getTitle(), fileName, + model.getTitle().getWikiSite(), getRevision(), GalleryFunnel.SOURCE_NON_LEAD_IMAGE), ACTIVITY_REQUEST_GALLERY, options.toBundle()); + }); + + ViewUtil.loadImage(viewForTransition, hitInfo.getSrc()); + }); + } else { Snackbar snackbar = FeedbackUtil.makeSnackbar(requireActivity(), getString(R.string.gallery_not_available_offline_snackbar), FeedbackUtil.LENGTH_DEFAULT); snackbar.setAction(R.string.gallery_not_available_offline_snackbar_dismiss, view -> snackbar.dismiss()); diff --git a/app/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java b/app/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java index 710092d52c1..f9ff6b4ee29 100755 --- a/app/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java +++ b/app/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.app.ActivityOptionsCompat; import androidx.fragment.app.FragmentActivity; import org.apache.commons.lang3.StringUtils; @@ -16,6 +17,7 @@ import org.wikipedia.WikipediaApp; import org.wikipedia.analytics.GalleryFunnel; import org.wikipedia.auth.AccountUtil; +import org.wikipedia.bridge.JavaScriptActionHandler; import org.wikipedia.commons.ImageTagsProvider; import org.wikipedia.dataclient.Service; import org.wikipedia.dataclient.ServiceFactory; @@ -243,16 +245,26 @@ public void onCallToActionClicked() { }); } - public void openImageInGallery(@Nullable String language) { + public void openImageInGallery(@Nullable String language) { if (getPage() != null && isLeadImageEnabled()) { String imageName = getPage().getPageProperties().getLeadImageName(); if (imageName != null) { String filename = "File:" + imageName; WikiSite wiki = language == null ? getTitle().getWikiSite() : WikiSite.forLanguageCode(language); + + JavaScriptActionHandler.ImageHitInfo hitInfo = new JavaScriptActionHandler.ImageHitInfo(pageHeaderView.image.getLeft(), + pageHeaderView.image.getTop(), pageHeaderView.image.getWidth(), pageHeaderView.image.getHeight(), + getLeadImageUrl(), true); + + GalleryActivity.setTransitionInfo(hitInfo); + + ActivityOptionsCompat options = ActivityOptionsCompat. + makeSceneTransitionAnimation(getActivity(), pageHeaderView.image, getActivity().getString(R.string.transition_page_gallery)); + getActivity().startActivityForResult(GalleryActivity.newIntent(getActivity(), parentFragment.getTitle(), filename, wiki, parentFragment.getRevision(), GalleryFunnel.SOURCE_LEAD_IMAGE), - Constants.ACTIVITY_REQUEST_GALLERY); + Constants.ACTIVITY_REQUEST_GALLERY, options.toBundle()); } } } diff --git a/app/src/main/java/org/wikipedia/views/ObservableWebView.java b/app/src/main/java/org/wikipedia/views/ObservableWebView.java index 68b27276382..38905067371 100755 --- a/app/src/main/java/org/wikipedia/views/ObservableWebView.java +++ b/app/src/main/java/org/wikipedia/views/ObservableWebView.java @@ -33,6 +33,8 @@ public class ObservableWebView extends WebView { private long lastScrollTime; private int totalAmountScrolled; private int drawEventsWhileSwiping; + private float lastTouchX; + private float lastTouchY; /** * Threshold (in pixels) of continuous scrolling, to be considered "fast" scrolling. @@ -159,6 +161,9 @@ public boolean onTouchEvent(MotionEvent event) { return true; } + lastTouchX = event.getX(); + lastTouchY = event.getY(); + switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: for (OnDownMotionEventListener listener : onDownMotionEventListeners) { @@ -193,6 +198,14 @@ public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); } + public float getLastTouchX() { + return lastTouchX; + } + + public float getLastTouchY() { + return lastTouchY; + } + private void handleMouseRightClick(float x, float y) { final int eventTimeTravelMillis = 1000; post(() -> dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), diff --git a/app/src/main/java/org/wikipedia/views/ViewUtil.java b/app/src/main/java/org/wikipedia/views/ViewUtil.java index 2f1223baa5a..f068d121858 100644 --- a/app/src/main/java/org/wikipedia/views/ViewUtil.java +++ b/app/src/main/java/org/wikipedia/views/ViewUtil.java @@ -20,6 +20,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestListener; import org.wikipedia.R; import org.wikipedia.util.DimenUtil; @@ -55,14 +56,23 @@ public static void loadImage(@NonNull ImageView view, @Nullable String url, bool } public static void loadImageWithWhiteBackground(@NonNull ImageView view, @Nullable String url) { + loadImageWithWhiteBackground(view, url, null); + } + + public static void loadImageWithWhiteBackground(@NonNull ImageView view, @Nullable String url, @Nullable RequestListener listener) { Drawable placeholder = getPlaceholderDrawable(view.getContext()); - Glide.with(view) + RequestBuilder builder = Glide.with(view) .load(!TextUtils.isEmpty(url) ? Uri.parse(url) : null) .placeholder(placeholder) .error(placeholder) .downsample(DownsampleStrategy.CENTER_INSIDE) - .transform(new WhiteBackgroundTransformation()) - .into(view); + .transform(new WhiteBackgroundTransformation()); + + if (listener != null) { + builder = builder.listener(listener); + } + + builder.into(view); } static Drawable getPlaceholderDrawable(@NonNull Context context) { diff --git a/app/src/main/res/layout/activity_gallery.xml b/app/src/main/res/layout/activity_gallery.xml index deaeea252dd..dd9cbe67927 100644 --- a/app/src/main/res/layout/activity_gallery.xml +++ b/app/src/main/res/layout/activity_gallery.xml @@ -13,6 +13,14 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + randomActivityTransition onThisDayTransition floatingQueue + pageGalleryTransition %d%%