Search code examples
androidgoogle-mapsandroid-fragmentsshared-element-transition

android-how to start a shared element transition from marker bitmap?


I'm using custom bitmaps as the icon of markers on the map. when the user clicks on any marker, I want to have a view corresponding to the clicked bitmap and add it as a shared element to the fragment transition. but I can't see any method to retrieve the bitmap from the marker. so how to start a shared element transition from the marker?


Solution

  • In short: use setTag()/getTag() methods to save/restore bitmap in marker object and additional ImageView with this bitmap as shared element for transitions between "map" and "details" fragments:

    shared marker icon

    TLDR;

    In case you "using custom bitmaps as the icon of markers" you can store them in Marker objects with setTag() method and then retrieve marker's icon bitmap in onMarkerClick(Marker marker) method. But bitmap is not enough for shared element transitions, because object of View class needed to perform it. So, you need to create additional View (e.g. ImageView) and use it as shared element for transitions between "map" and "details" fragments.

    In general you should:

    • On application start (in MainActivity):
    1. create "map" fragment with ImageView for shared element transition perform;
    2. create "details" fragment with correspondingImageView for shared element transition perform;
    3. create shared element transition animations;



    • When marker created (in "map" fragment):
    1. just save bitmap in marker object.



    • When user clicked on marker,

    in "map" fragment:

    1. in retrieve saved bitmap from marker object;
    2. set retrieved bitmap to shared ImageView;
    3. resize ImageView and move it to set exactly over the marker icon;
    4. hide marker icon and show ImageView with marker icon instead of marker;
    5. put marker bitmap (and e.g. description) to "details" fragment via arguments;
    6. create and start FragmentTransaction;

    in "details" fragment:

    1. get marker bitmap and description from arguments and show them on corresponding ImageView and TextView;



    • When user close "details" fragment(in "map" fragment),
    1. wait for transition animation end and show marker/hide ImageView.

    Most challenged part of that is create shared view inside "map" fragment, because SupportMapFragment did not have such element "from the box". So you need to create custom CustomSupportMapFragment that extends SupportMapFragment and have additional ImageView for shared element transitions:

    public class CustomSupportMapFragment extends SupportMapFragment {
        private Bitmap mBitmap;
        private float mY;
        private float mX;
    
        private RelativeLayout mRelativeLayout;
        private ImageView mSharedImageView;
    
        private Marker mMarker;
    
    
        @Override
        public View onCreateView(LayoutInflater inflater,
                                 @Nullable ViewGroup container,
                                 @Nullable Bundle savedInstanceState) {
            View root = super.onCreateView(inflater, container, savedInstanceState);
            mRelativeLayout = new RelativeLayout(root.getContext());
            mRelativeLayout.addView(root, new RelativeLayout.LayoutParams(-1, -1));
    
            mSharedImageView = new ImageView(root.getContext());
            mSharedImageView.setId(View.generateViewId());
            RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
            layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
            layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
            mSharedImageView.setLayoutParams(layoutParams);
    
            mSharedImageView.setTransitionName("sharedImageView");
    
            mRelativeLayout.addView(mSharedImageView);
    
            return mRelativeLayout;
        }
    
        @Override
        public void onStart() {
            super.onStart();
    
            if (mBitmap != null) {
                mSharedImageView.setImageBitmap(mBitmap);
                mSharedImageView.setX(mX);
                mSharedImageView.setY(mY);
            }
        }
    
        @Override
        public void onStop() {
            super.onStop();
            mBitmap = ((BitmapDrawable)mSharedImageView.getDrawable()).getBitmap();
            mX = mSharedImageView.getX();
            mY = mSharedImageView.getY();
        }
    
        public void setSharedMarker(Marker marker) {
            mMarker = marker;
        }
    
    
        public void setSharedViewInitialPosition(float x, float y) {
            mSharedImageView.setX(x);
            mSharedImageView.setY(y);
        }
    
        public void setSharedBitmap(Bitmap bitmap) {
            mSharedImageView.setImageBitmap(bitmap);
        }
    
        public ImageView getSharedView() {
            return mSharedImageView;
        }
    
        public void showMarker() {
            if (mMarker != null) mMarker.setVisible(true);
        }
    }
    

    "Details" fragment can be typical like that:

    public class DetailsFragment extends Fragment {
        private ImageView mImageView;
        private TextView mTextView;
    
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setSharedElementEnterTransition(TransitionInflater.from(getContext()).inflateTransition(android.R.transition.move));
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_details, container, false);
    
            mImageView = view.findViewById(R.id.picture_iv);
            mTextView = view.findViewById(R.id.details_tv);
    
            Bundle bundle = getArguments();
            if (bundle != null) {
                Bitmap bitmap = getArguments().getParcelable("image");
                mImageView.setImageBitmap(bitmap);
                String description = getArguments().getString("description");
                mTextView.setText(description);
            }
    
            return view;
        }
    }
    

    with layout like:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent" android:layout_height="match_parent">
    
        <ImageView
            android:id="@+id/picture_iv"
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:transitionName="sharedImageView"
            android:layout_centerInParent="true"/>
    
        <TextView
            android:id="@+id/details_tv"
            android:layout_below="@+id/picture_iv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:textSize="28dp"
            android:text="Description"/>
    
    </RelativeLayout>
    

    NB! Same "sharedImageView" name of transition needed across all "map" and "details" fragments views.

    And MainActivity should implement marker click processing logic:

    public class MainActivity extends AppCompatActivity {
        static final LatLng KYIV = new LatLng(50.450311, 30.523730);
        static final LatLng DNIPRO = new LatLng(48.466111, 35.025278);
    
        private GoogleMap mGoogleMap;
        private CustomSupportMapFragment mapFragment;
        private DetailsFragment detailsFragment;
    
        public class DetailsEnterTransition extends TransitionSet {
            public DetailsEnterTransition() {
                setOrdering(ORDERING_TOGETHER);
                addTransition(new ChangeBounds()).
                        addTransition(new ChangeTransform()).
                        addTransition(new ChangeImageTransform());
            }
        }
    
        public class DetailsExitTransition extends TransitionSet {
            public DetailsExitTransition(final CustomSupportMapFragment mapFragment) {
                setOrdering(ORDERING_TOGETHER);
                addTransition(new ChangeBounds()).
                        addTransition(new ChangeTransform()).
                        addTransition(new ChangeImageTransform());
                addListener(new TransitionListener() {
                    @Override
                    public void onTransitionStart(Transition transition) {
    
                    }
    
                    @Override
                    public void onTransitionEnd(Transition transition) {
                        if (mapFragment != null) {
                            mapFragment.showMarker();
                            mapFragment.setSharedBitmap(null);
                        }
                    }
    
                    @Override
                    public void onTransitionCancel(Transition transition) {
    
                    }
    
                    @Override
                    public void onTransitionPause(Transition transition) {
    
                    }
    
                    @Override
                    public void onTransitionResume(Transition transition) {
    
                    }
                });
            }
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            // create "map" fragment
            mapFragment = new CustomSupportMapFragment();
    
            // create "details" fragment and transitions animations
            detailsFragment = new DetailsFragment();
            detailsFragment.setSharedElementEnterTransition(new DetailsEnterTransition());
            detailsFragment.setSharedElementReturnTransition(new DetailsExitTransition(mapFragment));
    
            // show "map" fragment
            getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.container, mapFragment, "map")
                    .commit();
    
            // get GoogleMap object
            mapFragment.getMapAsync(new OnMapReadyCallback() {
                @Override
                public void onMapReady(GoogleMap googleMap) {
                    mGoogleMap = googleMap;
    
                    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_kyiv);
                    Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, 200, 250, false);
    
                    Marker marker = mGoogleMap.addMarker(new MarkerOptions()
                            .position(KYIV)
                            .icon(BitmapDescriptorFactory.fromBitmap(resizedBitmap))
                            .title("Kyiv"));
                    marker.setTag(resizedBitmap);  // save bitmap1 as tag of marker object
                    mGoogleMap.animateCamera(CameraUpdateFactory.newLatLng(KYIV));
    
                    bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_dnipro);
                    resizedBitmap = Bitmap.createScaledBitmap(bitmap, 200, 250, false);
                    marker = mGoogleMap.addMarker(new MarkerOptions()
                            .position(DNIPRO)
                            .icon(BitmapDescriptorFactory.fromBitmap(resizedBitmap))
                            .title("Dnipro"));
                    marker.setTag(resizedBitmap); // save bitmap2 as tag of marker object
    
                    mGoogleMap.animateCamera(CameraUpdateFactory.newLatLng(KYIV));
    
                    mGoogleMap.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() {
                        @Override
                        public boolean onMarkerClick(Marker marker) {
                            // retrieve bitmap for marker
                            Bitmap sharedBitmap = (Bitmap)marker.getTag();
    
                            // determine position of marker and shared element on screen
                            Projection projection = mGoogleMap.getProjection();
                            Point viewPosition = projection.toScreenLocation(marker.getPosition());
                            final float x = viewPosition.x - sharedBitmap.getWidth() / 2.0f;
                            final float y = viewPosition.y - sharedBitmap.getHeight();
    
                            // show shared ImageView and hide marker
                            mapFragment.setSharedMarker(marker);
                            mapFragment.setSharedBitmap(sharedBitmap);
                            mapFragment.setSharedViewInitialPosition(x, y);
                            mapFragment.getSharedView().setVisibility(View.VISIBLE);
                            mapFragment.getSharedView().invalidate();
                            marker.setVisible(false);
    
                            // prepare data for "details" fragment
                            Bundle bundle = new Bundle();
                            bundle.putParcelable("image", sharedBitmap);
                            bundle.putString("description", marker.getTitle());
                            detailsFragment.setArguments(bundle);
    
                            // create and start shared element transition animation
                            FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
                            ft.addSharedElement(mapFragment.getSharedView(), mapFragment.getSharedView().getTransitionName());
                            ft.replace(R.id.container, detailsFragment, "details");
                            ft.addToBackStack("details");
                            ft.commit();
    
                            return true; // prevent centring map on marker
                        }
                    });
                }
            });
    
        }
    
    }
    

    And that's it. Note: this is not fully-functional commercial code, just illustration.