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?

Answer

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.

Leave a Reply

Your email address will not be published. Required fields are marked *