アンドロイドのドキュメントによると、共有要素**の遷移は、2つの activities
の間で共有されるビューが、これらの activities
の間でどのように遷移するかを決定します。例えば、2つの activities
が同じ画像を異なる位置とサイズで持っている場合、changeImageTransform 共有要素の遷移は、これらの activities
の間で画像をスムーズに変換およびスケーリングします。
このスレッドでは、アンドロイドの共有トランジションに関連する例やライブラリを紹介します。このスレッドでは、アンドロイドの共有トランジションに関する例やライブラリを紹介します。
画像にSharedTransitionを簡単に実装する
Transitional ImageViewというライブラリを使えば、画像にも簡単に共有要素の遷移を実装できます。
その方法を紹介しましょう。
ステップ1 - ライブラリのインストール
まず、アプリレベルの build.gradle
に jitpack をリポジトリとして登録します。
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
次に、ライブラリをインストールします。
implementation 'com.github.mostafaaryan:transitional-imageview:v0.2.2'
ステップ 2
以下のコードを貼り付けて、レイアウトに Transitional ImageView を作成します。
<com.mostafaaryan.transitionalimageview.TransitionalImageView
android:id="@+id/transitional_image"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:scaleType="fitXY"
android:adjustViewBounds="true"
app:res_id="@drawable/sample_image" />
ステップ3
TransitionImageObjectを作成し、TransitionalImageViewに設定します。
TransitionalImageView transitionalImageView = (TransitionalImageView) findViewById(R.id.transitional_image);
TransitionalImage transitionalImage = new TransitionalImage.Builder()
.duration(500)
.backgroundColor(ContextCompat.getColor(MainActivity.this, R.color.color))
.image(R.drawable.sample_image)
/* or */
.image(bitmap)
.create();
transitionalImageView.setTransitionalImage(transitionalImage);
実例
このライブラリを使った美しい例を紹介します。
(a). Shoe.java (靴)
一つの靴を定義するモデルクラスです。
public class Shoe {
private String Title;
private String imageUrl;
public Shoe(String title, String imageUrl) {
Title = title;
this.imageUrl = imageUrl;
}
public String getTitle() {
return Title;
}
public String getImageUrl() {
return imageUrl;
}
}
(b). ShoeAdapter.java (シューアダプター
では、リサイクラービューのアダプタです。
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.ariannejad.mostafa.transitional_imageview_implementation.R;
import com.ariannejad.mostafa.transitional_imageview_implementation.controller.MainActivity;
import com.ariannejad.mostafa.transitional_imageview_implementation.model.Shoe;
import com.mostafaaryan.transitionalimageview.TransitionalImageView;
import com.mostafaaryan.transitionalimageview.model.TransitionalImage;
import com.squareup.picasso.Picasso;
import java.io.IOException;
import java.util.ArrayList;
/**
* Created by Mostafa Aryan Nejad on 8/11/17.
*/
public class ShoeAdapter extends RecyclerView.Adapter<ShoeAdapter.ViewHolder> {
Context mContext;
ArrayList<Shoe> shoes = new ArrayList<>();
public ShoeAdapter(Context context, ArrayList<Shoe> shoes) {
mContext = context;
this.shoes = shoes;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_shoe, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
final Shoe shoe = shoes.get(position);
AsyncTask.execute(new Runnable() {
@Override
public void run() {
try{
final Bitmap bitmap = Picasso.with(mContext).load(shoe.getImageUrl()).get();
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
TransitionalImage transitionalImage = new TransitionalImage.Builder()
.duration(500)
/*.backgroundColor(ContextCompat.getColor(, R.color.colorAccent))*/
/*.image(R.drawable.sample_image)*/
.image(bitmap)
.create();
holder.image.setTransitionalImage(transitionalImage);
bitmap.recycle();
}
});
} catch (IOException e){e.printStackTrace();}
}
});
holder.title.setText(shoe.getTitle());
holder.sizes.setText("37,38,39,40");
}
@Override
public int getItemCount() {
return shoes.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView title;
public TextView sizes;
public TransitionalImageView image;
public ViewHolder(View itemView) {
super(itemView);
title = (TextView) itemView.findViewById(R.id.shoe_title);
sizes = (TextView) itemView.findViewById(R.id.shoe_sizes);
image = (TransitionalImageView) itemView.findViewById(R.id.shoe_image);
}
}
}
(c). ShoeListActivity.java (シューリスト・アクティビティ)
シューズリストの activity
です。
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CollapsingToolbarLayout;
import android.support.design.widget.TabLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import com.ariannejad.mostafa.transitional_imageview_implementation.R;
import com.ariannejad.mostafa.transitional_imageview_implementation.adapter.ShoeAdapter;
import com.ariannejad.mostafa.transitional_imageview_implementation.model.Shoe;
import java.util.ArrayList;
public class ShoeListActivity extends AppCompatActivity {
private RecyclerView shoeRecyclerView;
private ArrayList<Shoe> shoes = new ArrayList<>();
private ActionBar actionBar;
private AppBarLayout appBarLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_shoe_list);
shoeRecyclerView = (RecyclerView) findViewById(R.id.shoe_recycler_view);
CollapsingToolbarLayout collapsingToolbar =
(CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
appBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout);
setOnOffsetChangedListener();
collapsingToolbar.setTitleEnabled(false);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
actionBar = getSupportActionBar();
if(actionBar != null) actionBar.setTitle("");
populateList();
}
private void populateList() {
shoes.add(new Shoe("Skechers Relaxed Fit Empire Game On Walking Shoe",
"https://www.shoes.com/pm/skech/skech800828_42965_hd2.jpg"));
shoes.add(new Shoe("Skechers After Burn Memory Fit Geardo High Top Trainer",
"https://www.shoes.com//pm/skech/skech798492_42965_hd2.jpg"));
shoes.add(new Shoe("New Balance Fresh Foam Zante v3 Running Shoe",
"https://www.shoes.com/pi/newba/hd/newba805216_436896_hd.jpg"));
for(int i = 0 ; i <= 5 ; i++ ) {
shoes.addAll(shoes);
}
displayList();
}
private void displayList() {
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
RecyclerView.Adapter adapter = new ShoeAdapter(this, shoes);
shoeRecyclerView.setLayoutManager(layoutManager);
shoeRecyclerView.setAdapter(adapter);
}
private void setOnOffsetChangedListener() {
appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
boolean isDisplayed = false;
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
int totalScroll = appBarLayout.getTotalScrollRange();
if (totalScroll + verticalOffset == 0) {
if (actionBar != null) {
actionBar.setTitle("Sneakers");
}
isDisplayed = true;
} else if (isDisplayed) {
if (actionBar != null)
actionBar.setTitle("");
isDisplayed = false;
}
}
});
}
}
(d). MainActivity.java (メインアクティビティ
最後にメインの activity
です。
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import com.ariannejad.mostafa.transitional_imageview_implementation.R;
import com.mostafaaryan.transitionalimageview.TransitionalImageView;
import com.mostafaaryan.transitionalimageview.model.TransitionalImage;
import com.squareup.picasso.Picasso;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
private String imageUrl = "https://image.freepik.com/free-icon/android-logo_318-54237.jpg";
TransitionalImageView tiv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tiv = (TransitionalImageView) findViewById(R.id.sample_image);
loadImage();
}
private void loadImage() {
/*
ImageLoader imageLoader;
imageLoader = ImageLoader.getInstance();
imageLoader.init(ImageLoaderConfiguration.createDefault(this));
AsyncTask.execute(new Runnable() {
@Override
public void run() {
DisplayImageOptions dio = new DisplayImageOptions.Builder()
.cacheInMemory(false).build();
final Bitmap bmp = imageLoader.loadImageSync(imageUrl, dio);
runOnUiThread(new Runnable() {
@Override
public void run() {
tiv.setImage(bmp);
}
});
}
});*/
/* Glide.with(this).asBitmap().load(imageUrl).into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) {
tiv.setImage(resource);
}
}); */
AsyncTask.execute(new Runnable() {
@Override
public void run() {
try {
final Bitmap b = Picasso.with(MainActivity.this).load(imageUrl).get();
runOnUiThread(new Runnable() {
@Override
public void run() {
// tiv.setImage(b);
TransitionalImage transitionalImage = new TransitionalImage.Builder()
.duration(500)
.backgroundColor(ContextCompat.getColor(MainActivity.this, R.color.colorAccent))
//.image(R.drawable.sample_image)
.image(b)
.create();
tiv.setTransitionalImage(transitionalImage);
}
});
} catch (IOException e) {e.printStackTrace();}
}
});
}
public void onClickShoes(View view) {
startActivity(new Intent(this, ShoeListActivity.class));
}
}
デモ
プロジェクトを実行したときのデモを紹介します。
ダウンロード
ダウンロードのリンクを示します。
Kotlin Shared Transition
RecyclerView and Fragments
これもKotlinで書かれたシンプルな共有トランジションの例です。今回はリサイクラービューが2つのフラグメント
の間で共有される要素になっています。
ツール)
ここでは、以下の点に注意してください。
- プログラミング言語 - Kotlin
- 最小限のSDK - 21
1. トランジションの作成
リソースの下のtransitionsというフォルダに、以下のものを追加します。
(a). change_bounds.xml (b).
<?xml version="1.0" encoding="utf-8"?>
<transitionSet>
<changeBounds />
</transitionSet>
(b). change_image_transform.xml (a).
次に
<?xml version="1.0" encoding="utf-8"?>
<transitionSet>
<changeImageTransform />
</transitionSet>
2. レイアウトの設計
コードの中にレイアウトが含まれています。
3. コードの記述
今回はKotlinでコードを書きます。
(a). フラグメント1.ktについて
最初のfragment
のコードです。
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
import android.transition.ChangeBounds
import android.transition.ChangeImageTransform
class Fragment1: Fragment() {
private lateinit var lm: LinearLayoutManager
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.activity_main, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lm = LinearLayoutManager(context, LinearLayoutManager.VERTICAL,
false)
rv.layoutManager = lm
val adapter = MainActivity.Adapter()
rv.adapter = adapter
btn.setOnClickListener {
// val changeImageTransform =
// TransitionInflater.from(context).inflateTransition(R.transition.change_image_transform)
// val changeBoundsTransform =
// TransitionInflater.from(context).inflateTransition(R.transition.change_bounds)
sharedElementReturnTransition = ChangeBounds()
sharedElementEnterTransition = ChangeImageTransform()
exitTransition = ChangeBounds()
val fragment2 = Fragment2()
// Setup transition on second fragment
fragment2.sharedElementEnterTransition = ChangeBounds()
fragment2.enterTransition = ChangeBounds();
val firstVisiblePosition = lm.findFirstVisibleItemPosition()
val lastVisiblePosition = lm.findLastVisibleItemPosition()
val transaction = fragmentManager!!.beginTransaction()
.replace(R.id.container, fragment2, fragment2::class.java.simpleName)
.addToBackStack("name")
for (i in firstVisiblePosition..lastVisiblePosition) {
val holderForAdapterPosition =
rv.findViewHolderForAdapterPosition(i) as MainActivity.Adapter.Holder
val itemView = holderForAdapterPosition.itemView
transaction.addSharedElement(itemView, "unique_key_$i")
}
transaction.commit()
}
}
}
(b). Fragment2.kt
2つ目のfragment
に以下のコードを追加します。
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
class Fragment2: Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.activity_main, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val lm = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL,
false)
rv.layoutManager = lm
val adapter = MainActivity.Adapter()
rv.adapter = adapter
btn.setOnClickListener {
}
postponeEnterTransition()
}
override fun onStart() {
super.onStart()
rv.post {
startPostponedEnterTransition()
}
}
}
(c). ScndActivity.kt
続いて、2つ目の activity
です。
import android.os.Bundle
import android.os.PersistableBundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
class ScndActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val lm = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL,
false)
rv.layoutManager = lm
val adapter = MainActivity.Adapter()
rv.adapter = adapter
btn.setOnClickListener {
// val currentOrientation = lm.orientation
// if (currentOrientation == LinearLayoutManager.VERTICAL) {
// lm.orientation = LinearLayoutManager.HORIZONTAL
// } else {
// lm.orientation = LinearLayoutManager.VERTICAL
// }
// adapter.notifyItemRangeChanged(1, adapter?.itemCount ?: 0)
}
supportPostponeEnterTransition()
rv.post {
supportStartPostponedEnterTransition()
}
}
}
(d). MainActivity.kt。
そして最後にメインの activity
です。
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class MainActivity : AppCompatActivity() {
private lateinit var lm: LinearLayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.cont)
val fragment1 = Fragment1()
supportFragmentManager.beginTransaction()
.add(R.id.container, fragment1, Fragment1::class.java.simpleName)
.commit()
// lm = LinearLayoutManager(this, LinearLayoutManager.VERTICAL,
// false)
// rv.layoutManager = lm
// val adapter = Adapter()
// rv.adapter = adapter
// btn.setOnClickListener {
// val firstVisiblePosition = lm.findFirstVisibleItemPosition()
// val lastVisiblePosition = lm.findLastVisibleItemPosition()
// val pairs = ArrayList<Pair<View, String>>()
// for (i in firstVisiblePosition..lastVisiblePosition) {
// val holderForAdapterPosition =
// rv.findViewHolderForAdapterPosition(i) as Adapter.Holder
// val itemView = holderForAdapterPosition.itemView
// pairs.add(Pair(itemView, "unique_key_$i"))
// }
// val bundle = ActivityOptions.makeSceneTransitionAnimation(
// this,
// *pairs.toTypedArray()
// ).toBundle()
// val fragment1 = Fragment1()
// supportFragmentManager.beginTransaction()
// .add(fragment1, Fragment1::class.java.simpleName)
// .commit()
// startActivity(Intent(this, ScndActivity::class.java), bundle)
// }
}
override fun onResume() {
super.onResume()
}
class Adapter : RecyclerView.Adapter<Adapter.Holder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder =
Holder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_item,
parent,
false
)
)
override fun getItemCount(): Int = 10
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bind(position)
}
class Holder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(position: Int) {
itemView.transitionName = "unique_key_$position"
}
}
}
}
デモ
このプロジェクトを実行すると、以下のようになります。
ダウンロード
Java Shared Transition
with Fragments and FloatingActionButton
これは、アンドロイドの「アクティビティ」で、「フラグメント」内の共有要素の遷移を利用するシンプルなワンクラスの例です。プログラミング言語はJavaです。androidxで書かれていませんが、androidxのfragments
に簡単にアップデートすることができますし、サードパーティのライブラリも利用していません。
トランジション
トランジションは XML で書かれています。 基本的には、transition resource ディレクトリを作成し、XML を配置します。
(a). shared_enter_transition.xml (b).
以下はそのコードです。
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/default_anim_duration">
<changeTransform/>
<arcMotion
android:minimumHorizontalAngle="0"
android:minimumVerticalAngle="15"
android:maximumAngle="90" />
<changeBounds />
</transitionSet>
アクティビティ
以下は、アクティビティ
です。
(a). メインアクティビティ.javaを使用します。
ここでは、メインのアクティビティ
を紹介します。
import android.animation.Animator;
import android.app.Fragment;
import android.os.Bundle;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.ActionBarActivity;
import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
public class FabActivity extends ActionBarActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fab);
getFragmentManager()
.beginTransaction()
.add(R.id.frag_content, TitleFragment.newInstance())
.commit();
}
public static class TitleFragment extends Fragment {
public static TitleFragment newInstance() {
return new TitleFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_fab_title, container, false);
final View fabbutton = view.findViewById(R.id.fab);
fabbutton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final ControlsFragment controlsFragment = ControlsFragment.newInstance();
setupSharedElementTransition(controlsFragment);
Fade f = new Fade();
f.setStartDelay(250);
setExitTransition(f);
getFragmentManager()
.beginTransaction()
.replace(R.id.frag_content, controlsFragment)
.addToBackStack("controls")
.addSharedElement(fabbutton, "pause_button")
.commit();
}
});
return view;
}
private void setupSharedElementTransition(final ControlsFragment controlsFragment) {
Transition sharedTransition = TransitionInflater.from(getActivity()).inflateTransition(R.transition.shared_enter_transition);
controlsFragment.setSharedElementEnterTransition(sharedTransition);
controlsFragment.setSharedElementReturnTransition(sharedTransition);
sharedTransition.addListener(new Transition.TransitionListener() {
@Override
public void onTransitionEnd(Transition transition) {
controlsFragment.revealContent();
}
@Override
public void onTransitionStart(Transition transition) {
}
@Override
public void onTransitionCancel(Transition transition) {
}
@Override
public void onTransitionPause(Transition transition) {
}
@Override
public void onTransitionResume(Transition transition) {
}
});
}
}
public static class ControlsFragment extends Fragment {
public static ControlsFragment newInstance() {
return new ControlsFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_fab_controls, container, false);
}
public void revealContent() {
View layout = getView().findViewById(R.id.controls_layout);
animateRevealColor(layout);
}
private void animateRevealColor(View targetView) {
int cx = (targetView.getLeft() + targetView.getRight()) / 2;
int cy = (targetView.getTop() + targetView.getBottom()) / 2;
cx += targetView.getTranslationX();
cy += targetView.getTranslationY();
int finalRadius = Math.max(targetView.getWidth(), targetView.getHeight());
Animator anim = ViewAnimationUtils.createCircularReveal(targetView, cx, cy, 0, finalRadius);
targetView.setBackgroundColor(getResources().getColor(R.color.accent_material_light));
anim.setDuration(getResources().getInteger(R.integer.default_anim_duration));
anim.setInterpolator(new AccelerateInterpolator());
anim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationEnd(Animator animator) {
animateScaleButton(getView().findViewById(R.id.ff_button));
animateScaleButton(getView().findViewById(R.id.rew_button));
}
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
anim.start();
}
private void animateScaleButton(View view) {
ViewCompat.animate(view)
.scaleX(1)
.scaleY(1)
.setDuration(250)
.start();
}
}
}
デモ
ここでは、プロジェクトを実行したときのデモを紹介します。