I will start off with a gif showing what is happening.
The list item is supposed to animate up to be the header item. As you can see, the header item animates back down to become the list item when the back button is pressed. There is another issue where the Activity parent for these fragments flashes behind the fade transition as well, so if you have any ideas about that I would like to know :)
Now, a little explanation of my code before I paste the snippets. This app navigates a tree structure of HTML documents. The small RecyclerView
list items are the titles of documents. When clicking on a document, it anchors the title on top with a WebView
that displays the HTML. There is only one fragment, and that fragment creates another instance of itself when a new fragment is created. The structure is a little weird, but the back button and stack seem to work as they should. This code is in C# because I am using Xamarin.Android, but most of it is very similar, aside from click handling.
I can start with my XML files. Layouts are still a WIP so I know they are messy.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/linear_layout_main"
android:background="#ffffff">
<LinearLayout
android:id="@+id/progressLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:visibility="gone"
android:gravity="center">
<ProgressBar
android:id="@+id/progressbar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="@+id/subroutines_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:background="#ffffff">
<TextView
android:id="@+id/routine_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="5dp"
android:textSize="24sp"
android:visibility="gone"/>
<FrameLayout
android:id="@+id/preview_frame"
android:layout_width ="match_parent"
android:layout_height="150dp"
android:visibility="gone">
<WebView
android:id="@+id/routine_preview"
android:layout_width="match_parent"
android:layout_height="150dp"
android:gravity="center_vertical"
android:padding="5dp"
android:transitionName="webview_preview_transition"
android:scrollbars="none"
android:visibility="gone"/>
<TextView
android:id="@+id/routine_preview_clickable"
android:layout_width="match_parent"
android:layout_height="150dp"
android:gravity="center_vertical"
android:visibility="gone"/>
</FrameLayout>
<TextView
android:id="@+id/preview_separator"
android:layout_width="match_parent"
android:layout_height="1dp"
android:gravity="center_vertical"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:background="@color/material_grey_100"
android:visibility="gone"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/routines_rv"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</android.support.v7.widget.RecyclerView>
</LinearLayout>
</LinearLayout>
This is the main layout for the fragment. The WebView
is inside of a frame, covered by an invisible TextView
because I need to be able to click it to expand the preview.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/routines_rv_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:gravity="center_vertical"
android:textSize="16sp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:background="@android:color/transparent"/>
This is the RecyclerView
list item.
namespace Routines.Views.Routines
{
class RoutineAdapter : RecyclerViewAdapter<PublishedDocumentFragmentDTO>
{
public RoutineAdapter(List<PublishedDocumentFragmentDTO> items,
int layout,
Func<View, Action<int>, HolderBase> viewHolderInitializer)
: base(items, layout, viewHolderInitializer) { }
public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
var vh = (RoutineHolder)holder;
vh.Text.Text = Items[position].Title;
vh.Text.TransitionName = "preview" + Items[position].Title;
}
public void UpdateData(List<PublishedDocumentFragmentDTO> routines)
{
Items = routines;
}
}
public class RoutineHolder : HolderBase
{
public TextView Text { get; private set; }
public RoutineHolder(View itemView, Action<int> action) : base(itemView, action)
{
Text = itemView.FindViewById<TextView>(Resource.Id.routines_rv_item);
}
}
}
The RecyclerViewAdapater
. Here, I set the TransitionName
for each item. I have checked to make sure that transition names match when starting the fragment, and in OnCreateView
, so I do not think those are the problem.
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var v = inflater.Inflate(Resource.Layout.routines_rv_fragment, container, false);
_subRoutinesLayout = v.FindViewById<LinearLayout>(Resource.Id.subroutines_layout);
_subRoutinesView = v.FindViewById<RecyclerView>(Resource.Id.routines_rv);
_progressLayout = v.FindViewById<LinearLayout>(Resource.Id.progressLayout);
_selectedTitleView = v.FindViewById<TextView>(Resource.Id.routine_title);
_selectedPreview = v.FindViewById<WebView>(Resource.Id.routine_preview);
_separator = v.FindViewById<TextView>(Resource.Id.preview_separator);
_previewFrame = v.FindViewById<FrameLayout>(Resource.Id.preview_frame);
_selectedPreviewClickable = v.FindViewById<TextView>(Resource.Id.routine_preview_clickable);
_subRoutinesView.HasFixedSize = true;
_subRoutinesView.SetLayoutManager(new LinearLayoutManager(Activity));
_subRoutinesView.AddItemDecoration(new MaterialDesignDivider(Activity, Resource.Drawable.md_divider));
_selectedPreview.TransitionGroup = true;
if (!_topLevel)
{
_selectedTitleView.Visibility = ViewStates.Visible;
_selectedPreview.Visibility = ViewStates.Visible;
_separator.Visibility = ViewStates.Visible;
_previewFrame.Visibility = ViewStates.Visible;
_selectedPreviewClickable.Visibility = ViewStates.Visible;
_selectedTitleView.TransitionName = "preview" + _selectedTitle;
_selectedTitleView.Text = _selectedTitle;
IRoutinesService _routinesService;
var routinesContainer = AutoFac.Container;
_routinesService = routinesContainer.Resolve<IRoutinesService>();
var blobIdGuid = new Guid(_selectedBlobId);
Tuple<bool, string> tuple = _routinesService.GetFragment(blobIdGuid);
if (tuple.Item1)
{
string mimeType = "text/html";
string encoding = "UTF-8";
string html = File.ReadAllText(tuple.Item2);
_selectedPreview.LoadDataWithBaseURL("", transformedHtml, mimeType, encoding, "");
}
_selectedPreviewClickable.Click += PreviewClicked;
}
return v;
}
This is the whole OnCreateView
method, for good measure. But I think the only part that will be relevant to my transition is
_selectedTitleView.TransitionName = "preview" + _selectedTitle;
_selectedTitleView.Text = _selectedTitle;
_selectedTitleView
is the TextView
that is anchored at the top. _selectedTitle
is passed in to the fragment as the title of the RecyclerView
item that was selected. _topLevel
is a bool
that is set to true
when this fragment is created from itself. This is because the first instance of this fragment does not have the anchored file at the top, so those views are not visible.
private void SubRoutineSelected(int position)
{
PublishedDocumentFragmentDTO selectedRoutine;
IRoutinesService _routinesService;
var container = AutoFac.Container;
_routinesService = container.Resolve<IRoutinesService>();
if (_topLevel)
{
selectedRoutine = _viewModel.SelectedRoutineTree[position];
}
else
{
var childList = _viewModel.RetrieveSelectedChildren(_fragId, _viewModel.SelectedRoutineTree);
selectedRoutine = childList[position];
}
var newFragId = selectedRoutine.FragmentId;
var newBlobId = selectedRoutine.BlobId;
var title = selectedRoutine.Title;
if (selectedRoutine.Children.Any())
{
SubroutinesFragment selectedRoutineFragment = new SubroutinesFragment(_viewModel, newFragId, false, title, newBlobId);
if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop)
{
SharedElementReturnTransition = new PreviewTransition();
ExitTransition = new Fade();
selectedRoutineFragment.SharedElementEnterTransition = new PreviewTransition();
selectedRoutineFragment.EnterTransition = new Fade();
}
var test = (RoutineHolder)_subRoutinesView.FindViewHolderForAdapterPosition(position);
FragmentManager.BeginTransaction().
Replace(Resource.Id.main_frame, selectedRoutineFragment).
AddToBackStack(null).
AddSharedElement(test.Text, test.Text.TransitionName).
Commit();
}
else
{
var intent = new Intent(Activity, typeof(RoutineActivity));
intent.PutExtra(RoutineBundleKeys.BLOB_ID, newBlobId);
intent.PutExtra(RoutineBundleKeys.ROUTINE_TITLE, title);
StartActivity(intent);
}
}
This is the code that is executed when a list item is clicked.
public class PreviewTransition : TransitionSet
{
public PreviewTransition()
{
SetOrdering(TransitionOrdering.Together);
AddTransition(new ChangeTransform()).
AddTransition(new ChangeBounds());
}
}
And finally, this is the class for the animation. Very simple.
So I hope SOMEBODY has an idea of what direction to look in. I really cannot figure out why the return animation works but not the start animation.
Just to clarify, this is a transition between two different fragments. The SubRoutineSelected
method can be considered the "First Fragment", and then OnCreateView
is the "Second Fragment".