preface
Always wanted to take the time to re-learn the native UI and animation of Apple products, super silky. Today, the target is the AppStore front page’s card stream and its transitions.
It should be noted that if the results of this reproduction are evaluated from static layout, dynamic switching effect (animation function curve, blur processing), detail presentation and other dimensions, this paper has only achieved the most important part of them, and there is considerable room for further optimization.
AppStore and Android implementation effect preview
(Video to GIF resulting in pixel compression is more severe, but please forgive me)
- The AppStore’s official card switching effect:
A word, silky; And the closing of the card detail page animation can be interrupted by touch operation.
- The card switching effect reproduced by Android in this paper:
The final implementation does not include the ability to swipe left back to the previous page, and there are still quite a few details that can be optimized, so there is a long way to go. 😥
1. Page content analysis
Before we start, we might as well take a closer look at the information on this page, translate it into our business needs, and organize our thoughts in advance before we start writing. While we observe, while carrying out the basic implementation ideas piecemeal, from now on will gradually mention some Android keywords.
1.1 Static Layout
Pictured above are the AppStore home page and the card details page
Let’s take a look at what the static layout covers. The entire AppStore front page is actually composed of two pages:
- Home page
- Page content: A stream of cards of the same size, each containing basic information about the page (main and subtitle, summary, background)
- Card style requirements: shadow around, card rounded corners, display background, content expansion (add text later)
We naturally thought of using CardView to do this. Shadows can be set with an elevation, rounded corners with a cornerRadius, and a LinearLayout and a ScrollView can be added to display the background and expand the content. Well, it fits. In addition, the card stream can be hosted by RecyclerView. Each ViewHolder holds a card, and we can realize the card stream by storing the title, abstract, and background ID of all the cards through ArrayList.
- Card Details page
- Page content: reuse all the elements of the first page card, and expand the card, add the body in. In addition, there is a page close button that is fixed in the upper right corner and does not change with the ScrollView.
Visually, the detail page is just a copy of the card, but with a body section added; From an implementation perspective, we also use a CardView as the basis for the details page, but add a ScrollView as the body bearer. This will make it easier to animate the shared elements later.
After looking at static pages, let’s organize our thoughts:
- First, we create the base style of the card, which we’ll callarticle_card_layout.xml
- Next, we prepare two
Fragment
, among them,HomeFragmentThe card stream for the home page,DetailFragmentA stream of cards for the detail page. - HomeFragmentThe RecyclerView is a RecyclerView, and the itemView in the ViewHolder is the one we created abovearticle_card_layout.
- DetailFragmentWill serve as ourObjects that are constantly reusedWe set each card’sClick on eventIn eachWhen the card is clicked“, we confirm the contents of all the elements of the upcoming details page, load the data in, render the animation, and open the new page.
The overall structure is shown in the figure below:
1.2 Dynamic page Switching Effect
Let’s start with the AppStore slowing down by 5 timesThe animation effect of the card opening:
As a whole, it looks like the bottom of a card has been slowly stretched and unfolded, popping out in front of us. Here, let’s take a closer look at what needs to be done in this animation:
- Shared elements: The background, main headings, and subheadings in the card should flow naturally into the detail page; This requires us to animate with shared elements. Details the Link between: use the transition to layout changes add animations | | Android Developers Android Developers (Google, cn)
- Card size and outline: The moment the card is clicked/touched first shrinks and then pops up,The overall animation looks like a spring, we can use Android built-inAnimation interpolator OverShootInterpolatorTo achieve a spring-like animation curve; We use shared elements in the animation
<changeBound>
with<changeTransform>
To achieve card size, contour change. - Round Corner: The Corner of the card will gradually decrease to 0, which needs to be realized by ourselves.
- Blurring of other cards on the home page: After clicking on a card, other elements of the home page will gradually blur to highlight the main body.
2. Page implementation
Now that we’re done, let’s write. In order to ensure the readability of the article, the code here will only put the core part, the students who really need the code can get at the end of the article.
We will proceed step by step along the previous ideas.
2.1 Card flow & static layout of card detail page
- First, create our card (article_card_layout.xml), as mentioned earlier, we will use CardView as the base carrier
- Create a CardView:
- The default elevation=2 helps us implement shadows
- Set cardCornerRadius=14dp to round the card
- Create a LinearLayout inside the CardView
- Create three TextViews inside it to hold the main, subtitle and summary, respectively
- Using the backGround of this LinearLayout as the container for our backGround image, we’ll put a default one in first.
So far, we have the following XML structure and its preview:
- createHomeFragment, deal with the most importantRecyclerView
- We create the Adapter and ViewHolder required by RecyclerView
- We will create a data class, ArticleCardData, to store the text content and background image ID of each card. Of course, you can put anything in there that you want the card to be easy to customize.
data class ArticleCardData(
val backGroundImage: Int = R.drawable.testimg,
val cardTitle: String = "Latest".val mainTitle: String = "Extraordinarily,\nundefeated.".val rootText: String = "i-Sense makes life better.".val contentText: String = "".val mainTitleColor: Int = Color.parseColor("#fafdfb"))Copy the code
-
We need the ViewHolder to load the main, subtitle, background, etc. of the article each time the binding (onBindViewHolder) is performed
-
Initialize RecyclerView, set adpater, layoutManager, etc.
Here, it should be noted that in RecyclerView, if we want to realize the upper, lower, left and right spacing of each Item, we need to create an Item decoration class to install the Item and realize the spacing effect.
class CardItemDecoration : RecyclerView.ItemDecoration() {
private val itemSpaceDistance = 24f.dp.toInt()
private val horizontalSpace = 18f.dp.toInt()
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
outRect.apply {
this.left = horizontalSpace
this.right = horizontalSpace
this.bottom = itemSpaceDistance
}
if (parent.getChildAdapterPosition(view) == 0) {
outRect.top = itemSpaceDistance
}
}
}
Copy the code
- Create a click event for each card, jump to the DetailFragment, and load the card’s corresponding data into it:
Here, I use the ViewModel to implement data transfer between fragments: Set the ViewModel Provider to the Activity, so that our ViewModel life cycle changes with the Activity to help us implement data transfer.
- Initialize the ViewModel so that its lifecycle follows the activity
// We are at homefragment.kt
articleCardViewModel = ViewModelProvider(activity).get(ArticleDetailViewModel::class.java)
Copy the code
- Fetch the viewModel in the same manner within any fragment of the activity
// We are in detailFragment.kt viewModel = ViewModelProvider(Activity!!) .get(ArticleDetailViewModel::class.java)Copy the code
- Card click event: The current card passes the values of this card to the viewModel, which is then received by the DetailFragment, which retrieves these values when rendering its own page and becomes the detail page of that card.
// Add click events to each Item of recyclerView
override fun onItemClick(viewHolder: RecyclerView.ViewHolder?). {
varposition = cardRecyclerView.getChildLayoutPosition(viewHolder!! .itemView) GlobalScope.launch(Dispatchers.Default) {// Update the main, subtitle, summary, etc
articleDetailViewModel.articleCardData = cardArray[position]
// Update the background image
articleDetailViewModel.updateBackGroundImage(resources, activity!!)
// Pass in the position of the current item
articleDetailViewModel.position = position.toString()
}
// Use Navigation to jump to the next page.
}
Copy the code
On the basis of article_detail_layout. XML, a layer of ScrollView is added externally to show the long text, and a contentText TextView is added internally. The overall structure and preview are as follows:
The DetailFragment receives the data and renders its own image:
//in DetailFragment.kt
// Card-related data passed in viewModel
viewModel.articleCardData.apply {
view.findViewById<TextView>(R.id.mainTitle).text = this.mainTitle
view.findViewById<TextView>(R.id.cardTitle).text = this.cardTitle
view.findViewById<TextView>(R.id.rootText).text = this.rootText
view.findViewById<TextView>(R.id.mainTitle).setTextColor(this.mainTitleColor)
// Set the body
if (this.contentText ! ="") {
view.findViewById<TextView>(R.id.contentText).text = this.contentText
}
// Set the background image
view.findViewById<LinearLayout>(R.id.cardLinearLayout).background = viewModel.backGroundImage
}
view.findViewById<CardView>(R.id.backGroundCard).transitionName = "backGroundCard${viewModel.position}"
Copy the code
- We have now completed the layout of the static page. Finally, use pictures to sort out the process!
2.2 Transition animation between card and detail page
Finally, we come to the most interesting part, which is the core role: SharedElementTransition Shared element animation
Introduction to the use of shared element animation
Official introduction of Shared elements animation please jump: using transition to layout changes add animations | | Android Developers Android Developers (Google, cn)
Attached is a library of shared element animations that are used a lot: Material-Motion
Here, I would like to introduce it in my own way:
- Animation of shared elements can be used between fragments and activities. It is very easy to use. You just need to ensure that the shared element’s TransitionName is the same in both fragments and bind it before jumping.
- During this Transition, we can specify a Transition animation to achieve the desired effect, such as Fade() to Fade in and out, and ChangeTransform() to change the size.
- The bottom layer of the Transition animation is a property animation, and it’s going to get some value of the shared element in FragmentA as a starting point, like x=0,y=0, and then it’s going to get some value of the shared element in FragmentB as a ending point, x=100,y=100, and then it’s going to do a property animation. To allow the shared element to move smoothly across.
- With this in mind, we can easily customize Transition by rewriting a few methods, controlling the values we want to start and finish, and defining the properties we want to animate. Specific can see the official document: create a custom transition animations | | Android Developers Android Developers (Google, cn)
In RecyclerView, animate items as shared elements
In RecycerView, the TransitionName of the Fragment must be the same as the TransitionName of the Fragment’s shared element.
- When creating these card streams, we assign each card’s TransitionName to “shared_card${position}”, position being its position, to ensure that their TransitionName is unique.
- Next, we pass the DetailFragment TransitionName of the currently clicked card after the card is clicked, and ask the DetailFragment to modify its own card component TransitionName to “shareD_card ${position}”.
In this way, we have achieved the binding.
Add a Navigation jump to each Item click event! (You can also use FragmentManager) :
A. We need to create a binding between the current View and its TransitionName (the naming rules mentioned above).
// First create a binding of the form view to TransitionName
val extras = FragmentNavigatorExtras(
viewHolder.itemView.findViewById<CardView>(R.id.backGroundCardView) to "backGroundCard${position}".)Copy the code
B. We then use navigate() for the jump, and inside the function we insert the target fragment ID with the extras previously bound
view!! .findNavController().navigate( R.id.action_to_article,null.null,
extras
)
Copy the code
To complete the last step of the shared element animation, set the Transition effect we want in the DetailFragment. SharedElementEnterTransition object to accept a Transition, the Transition is contains the we need to implement animation effects. The r.triton. Shared we use here is a custom Transition set.
//in DetailFragment.kt
sharedElementEnterTransition = TransitionInflater.from(requireContext()).inflateTransition(R.transition.shared)
sharedElementReturnTransition = TransitionInflater.from(requireContext()).inflateTransition(R.transition.shared)
Copy the code
We’re using a shared element animation Transition: r.tensition.shared
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="together"
android:duration="400">
<transitionSet android:transitionOrdering="together">
<transition class="isense.com.ui.myTransition.MyCornerTransition">
</transition>
</transitionSet>
<changeBounds android:interpolator="@anim/my_overshoot">
</changeBounds>
<changeTransform android:interpolator="@anim/my_overshoot">
</changeTransform>
</transitionSet>
Copy the code
In the code above, we define Transition with three elements: changeBounds, CornerTransiton, and changeTransform. We use them to achieve the desired card expansion effect.
Why use OverShootInterpolator?
As mentioned earlier, the AppStore native animation function curve is spring-like, which is similar to the function curve OverShootInterpolator:
When they reach their target, they take a small step forward and then back again, just like the function below:
How to achieve other card blur?
Here, I use Github’s open source library wasabeef/Blurry: Blurry is an easy blur library for Android (github.com). Blurry is an easy blur library for Android (github.com). Blurry is an easy blur library for Android (github.com).
viewHolder.itemView.visibility=View.INVISIBLE
Blurry.with(context).radius(25).sampling(1).animate(100).onto(NoiseConstraintLayout)
viewHolder.itemView.visibility=View.VISIBLE
Copy the code
Finally, to ensure that the shared animation returns, please note:
To ensure that the DetailFragment returns the same animation as the HomeFragment, add the following code to onCreate() in the HomeFragment:
postponeEnterTransition()
view.doOnPreDraw { startPostponedEnterTransition() }
Copy the code
This is the end of the article, if you need the complete code can leave a message; If some students need it, I will sort it out and put it on Git as soon as possible.