From API 21, Material Design has bring us the new way to switchMotion in the world of material design is used to describe spatial relationships, functionality, and intention with beauty and fluidity. Motion design can effectively guide the user’s attention in ways that both inform and delight. Use motion to smoothly transport users between navigational contexts, explain changes in the arrangement of elements on a screen, and reinforce element hierarchy.
Activity
(activity transition) with animations and of course, the element located on these screen are also affected by this transition process.We must apply those animations carefully to avoid the app become a true Pixar animation movie. In this post, I will present some customizing of Material meaningful motion, make our application look smoothly.
DEMO VIDEO:
Prerequisites
app/build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 24
buildToolsVersion "24.0.2"
defaultConfig {
applicationId "info.devexchanges.uimotion"
minSdkVersion 21
targetSdkVersion 24
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:24.2.0'
compile 'com.android.support:design:24.2.0'
compile 'com.android.support:gridlayout-v7:24.2.0'
compile 'com.android.support:cardview-v7:24.2.0'
}
Custom Activity Transitions in xml
Suppose I have 3 activities: the first called
MainActivity
which display 3 pictures and after click at any one, app will redirect user to second activity to show selected picture descriptions (called DetailsActivity
). This activity contains a FloatingActionButton
, when click on it, a translucent activity (SharingActivity
) appears which give some options to shared the content. It has a yellow shape which is a drawable defined as image source of a ImageView
. Now, we'll custom some animations based on xml resources.Create a folder named
transaction
in res
directory, all animations xml files will put here.We can specify custom animations for enter and exit transitions and for transitions of shared elements between activities.
- An enter transition determines how views move into the initial scene of the started activity.
- An exit transition determines how views move out of the scene when starting a new activity.
- A shared elements transition determines how views are shared between two activities transition.
MainActivity
to DetailsActivity
: and showing translucent SharingActivity
:
main_reenter.xml
<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/android"
android:slideEdge="top">
<targets>
<target android:excludeId="@android:id/statusBarBackground" />
<target android:excludeId="@android:id/navigationBarBackground" />
</targets>
</slide>
main_exit.xml
<?xml version="1.0" encoding="utf-8"?>
<explode xmlns:android="http://schemas.android.com/apk/res/android">
<targets>
<target android:excludeId="@android:id/statusBarBackground" />
<target android:excludeId="@android:id/navigationBarBackground" />
</targets>
</explode>
detail_enter.xml
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="together">
<slide
android:slideEdge="bottom">
<targets>
<target android:targetId="@id/cardview"/>
</targets>
</slide>
<fade>
<targets>
<target android:excludeId="@android:id/statusBarBackground"/>
<target android:excludeId="@android:id/navigationBarBackground"/>
<target android:excludeId="@id/cardview"/>
</targets>
</fade>
</transitionSet>
sharing_shared_element_enter.xml
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/accelerate_decelerate">
<changeBounds/>
<arcMotion
android:maximumAngle="90"
android:minimumHorizontalAngle="90"
android:minimumVerticalAngle="0"/>
</transitionSet>
sharing_item_chosen.xml
Activity transition definitions can be declared into theme style and our res/values/styles.xml contains all this:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeBounds/>
<fade>
<targets>
<target android:excludeId="@id/content_root"/>
</targets>
</fade>
<changeImageTransform android:startDelay="@android:integer/config_mediumAnimTime"/>
</transitionSet>
styles.xml
And never forget to use the correct theme for each activity in AndroidManifest.xml:
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.Main">
<item name="android:windowExitTransition">@transition/main_exit</item>
<item name="android:windowReenterTransition">@transition/main_reenter</item>
</style>
<style name="AppTheme.Detail">
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowAllowEnterTransitionOverlap">false</item>
<item name="android:windowEnterTransition">@transition/detail_enter</item>
</style>
<style name="AppTheme.Sharing">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@color/black</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowSharedElementEnterTransition">@transition/sharing_shared_element_enter</item>
</style>
<style name="ShareItemView">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:background">?android:attr/selectableItemBackgroundBorderless</item>
<item name="android:textAppearance">@style/TextAppearance.AppCompat.Medium.Inverse</item>
</style>
</resources>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="info.devexchanges.uimotion">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme.Main">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".DetailsActivity"
android:theme="@style/AppTheme.Detail"/>
<activity android:name=".SharingActivity"/>
</application>
</manifest>
Transition from Main screen to Detail screen
DetailsActivity
with some information that indicates we’re starting a Customized Activity Transition.
In MainActivity
we have:
@Override
public void onClick(View view) {
if (view.getId() == R.id.rose) {
openDetailActivity(R.drawable.rose, "Rose", view);
} else if (view.getId() == R.id.sunflower) {
openDetailActivity(R.drawable.sunflower, "Sunflower", view);
} else {
openDetailActivity(R.drawable.tulip, "Tulip", view);
}
}
private void openDetailActivity(int drawable, String title, View view) {
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this, view, getString(R.string.picture_transition_name));
Intent intent = new Intent(this, DetailsActivity.class);
intent.putExtra(DetailsActivity.EXTRA_DRAWABLE, drawable);
intent.putExtra(DetailsActivity.EXTRA_TITLE, title);
startActivity(intent, options.toBundle());
}
ActivityOptions.makeSceneTransitionAnimation()
. It create an object containing information about our scene transition animation.As you see, I pass drawable id and string title from
MainActivity
to setup CollapsingToolbarLayout
and ImageView
in DetailsActivity
. To finish the our motion from main to details screen we just scale up the share button when the transition is ended:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_details);
setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
int drawable = getIntent().getExtras().getInt(EXTRA_DRAWABLE);
CharSequence title = getIntent().getExtras().getCharSequence(EXTRA_TITLE);
CollapsingToolbarLayout collapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
collapsingToolbarLayout.setTitle(title);
ImageView pictureView = (ImageView) findViewById(R.id.picture);
pictureView.setImageResource(drawable);
pictureView.setContentDescription(title);
btnShare = findViewById(R.id.btn_share);
textView = (TextView) findViewById(R.id.text);
if (drawable == R.drawable.rose) {
textView.setText(getString(R.string.rose));
} else if (drawable == R.drawable.tulip) {
textView.setText(getString(R.string.tulip));
} else textView.setText(getString(R.string.sunflower));
if (savedInstanceState == null) {
btnShare.setScaleX(0);
btnShare.setScaleY(0);
getWindow().getEnterTransition().addListener(new TransitionAdapter() {
@Override
public void onTransitionEnd(Transition transition) {
getWindow().getEnterTransition().removeListener(this);
btnShare.animate().scaleX(1).scaleY(1);
}
});
}
}
@Override
public void onBackPressed() {
btnShare.animate().scaleX(0).scaleY(0).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
supportFinishAfterTransition();
}
});
}
And when running app, we have this output:
Transition from Detail screen to Sharing screen
SharingActivity
after click on the FloatingActionButton
:
btnShare.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(DetailsActivity.this,
btnShare, getString(R.string.share_transition_name));
Intent intent = new Intent(DetailsActivity.this, SharingActivity.class);
startActivity(intent, options.toBundle());
}
});
In SharingActivity
, we have to setup initial states before the animation begin:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
rootView = (ViewGroup) findViewById(R.id.content_root);
backgroundView = (ImageView) findViewById(R.id.background);
btnFacebook = findViewById(R.id.facebook);
btnInstagram = findViewById(R.id.instagram);
btnTwitter = findViewById(R.id.twitter);
btnGoogle = findViewById(R.id.google_plus);
if (savedInstanceState == null) {
// Setup initial states
backgroundView.setVisibility(View.INVISIBLE);
btnGoogle.setAlpha(0);
btnTwitter.setAlpha(0);
btnFacebook.setAlpha(0);
btnInstagram.setAlpha(0);
}
getWindow().getSharedElementEnterTransition().addListener(new TransitionAdapter() {
@Override
public void onTransitionEnd(Transition transition) {
getWindow().getSharedElementEnterTransition().removeListener(this);
revealTheBackground();
showTheItems();
}
});
...
}
SharingActivity
is handling share items (buttons) click. The pure Transition Framework was added since Android API 19. This framework animates the views at runtime by changing some of their property values over time. One of the features is the ability of running animations based on the changes between starting and ending view property values:
@Override
public void onClick(View view) {
showToast(view.getId());
// Load the transition
Transition transition = TransitionInflater.from(this).inflateTransition(R.transition.sharing_item_chosen);
// Finish this Activity when the transition is ended
transition.addListener(new TransitionAdapter() {
@Override
public void onTransitionEnd(Transition transition) {
finish();
// Override default transition to fade out
overridePendingTransition(0, android.R.anim.fade_out);
}
});
// Capture current values in the scene root and then post a request to run a transition on the next frame
TransitionManager.beginDelayedTransition(rootView, transition);
// 1. Item chosen
RelativeLayout.LayoutParams layoutParams =
new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
view.setLayoutParams(layoutParams);
// 2. Rest of items
View[] itemViews = {btnFacebook, btnInstagram, btnTwitter, btnGoogle};
for (View itemView : itemViews) {
if (itemView != view) {
itemView.setVisibility(View.INVISIBLE);
}
}
// 3. Background
double diagonal = Math.sqrt(rootView.getHeight() * rootView.getHeight() + rootView.getWidth() * rootView.getWidth());
float radius = (float) (diagonal / 2f);
int h = backgroundView.getDrawable().getIntrinsicHeight();
float scale = radius / (h / 2f);
Matrix matrix = new Matrix(backgroundView.getImageMatrix());
matrix.postScale(scale, scale, backgroundView.getWidth() / 2f, backgroundView.getHeight() / 2f);
backgroundView.setScaleType(ImageView.ScaleType.MATRIX);
backgroundView.setImageMatrix(matrix);
}
onBackPressed()
to start the hide animation of item and background:
private void hideTheBackground() {
Animator hide = createRevealAnimator(false);
hide.setStartDelay(defaultAnimDuration);
hide.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
backgroundView.setVisibility(View.INVISIBLE);
supportFinishAfterTransition();
}
});
hide.start();
}
@Override
public void onBackPressed() {
hideTheItems();
hideTheBackground();
}
private void hideTheItems() {
View[] itemViews = {btnFacebook, btnInstagram, btnTwitter, btnGoogle};
for (int i = 0; i < itemViews.length; i++) {
View itemView = itemViews[i];
long startDelay = (defaultAnimDuration / itemViews.length) * (itemViews.length - i);
itemView.animate().alpha(0).setStartDelay(startDelay);
}
}
And we'll have this result:
Final thoughts
- Defining Custom Animation in Google training
- Material design motion in Google design