2016年11月24日木曜日

BottomNavigationView で画面回転時に位置を保持するようにしてみた

注意:以下の内容は Design Support Library v25.0.1 時点でのものです

v25.0.0 から Design Support Library に BottomNavigationView が追加されましたが、最新版(v25.0.1)でも画面回転時に選択アイテムの位置を保持してくれず、選択が一番最初のアイテムに戻ってしまう問題があります。しかも選択中のアイテムを変更するAPIも現状では用意されていません。

いちを以下の方法で選択アイテムを変更することはできます。
  1. final View view = findViewById(menuId);  
  2. if (view != null) {  
  3.     view.performClick();  
  4. }  
でももにょるよね...

本家が対応するまでの間、上記の苦し紛れの方法を駆使した CustomBottomNavigationView を用意しました。これで画面回転時も位置が保持されます。

CustomBottomNavigationView

ついでにこれを使って fragment の入れ替えもちゃんと実装したサンプルを用意したので、ぜひ参考にしてください。

https://github.com/yanzm/BottomNavigationSample


本家で早く対応してください。


2016年11月18日金曜日

SharedElement をフェードインさせたい

やりたいことは次の動画を見てもらうのが早いです。



言葉にすると、
1. Activity1 から Activity2 に遷移するときに、
2. ある View を sharedElement としてアニメーション(移動)させたい
3. Activity2 では sharedElement が移動している間に表示している内容を変更したい

Activity1 の方のレイアウトは ImageView 一つだけで、これが sharedElement の対象。
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <FrameLayout  
  3.     xmlns:android="http://schemas.android.com/apk/res/android"  
  4.     xmlns:tools="http://schemas.android.com/tools"  
  5.     android:layout_width="match_parent"  
  6.     android:layout_height="match_parent">  
  7.   
  8.     <ImageView  
  9.         android:id="@+id/image"  
  10.         android:layout_width="128dp"  
  11.         android:layout_height="128dp"  
  12.         android:layout_gravity="bottom"  
  13.         android:src="@drawable/sample_image1"  
  14.         android:transitionName="image"  
  15.         tools:ignore="ContentDescription"/>  
  16.   
  17. </FrameLayout>  
Activity2 の方は ImageView が2つ重なっていて、一つ目の ImageView には Activity1 と同じ画像、二つ目の ImageView には Transition 後に表示したい画像がセットされている。二つ目の ImageView は非表示(INVISIBLE)。二つの ImageView の container である FrameLayout が sharedElement の対象。
  1. <!--xml version="1.0" encoding="utf-8"?-->  
  2. <?xml version="1.0" encoding="utf-8"?>  
  3. <LinearLayout  
  4.     xmlns:android="http://schemas.android.com/apk/res/android"  
  5.     xmlns:tools="http://schemas.android.com/tools"  
  6.     android:layout_width="match_parent"  
  7.     android:layout_height="match_parent"  
  8.     android:orientation="vertical">  
  9.   
  10.     <net.yanzm.sample.SquareFrameLayout  
  11.         android:layout_width="match_parent"  
  12.         android:layout_height="wrap_content"  
  13.         android:transitionName="image">  
  14.   
  15.         <ImageView  
  16.             android:id="@+id/image"  
  17.             android:layout_width="match_parent"  
  18.             android:layout_height="match_parent"  
  19.             android:src="@drawable/sample_image1"  
  20.             tools:ignore="ContentDescription"/>  
  21.   
  22.         <ImageView  
  23.             android:id="@+id/image2"  
  24.             android:layout_width="match_parent"  
  25.             android:layout_height="match_parent"  
  26.             android:src="@drawable/sample_image2"  
  27.             android:visibility="invisible"  
  28.             tools:ignore="ContentDescription"/>  
  29.   
  30.     </net.yanzm.sample.SquareFrameLayout>  
  31.   
  32. </LinearLayout>  
Activity1側で ActivityOptionsCompat.makeSceneTransitionAnimation() を使って Activity2 を呼び出すと、画面遷移時に SharedElement が移動します。 当たり前ですがこの段階では Activity2 側の二つ目の ImageView は出てきません(INVISIBLEなので)。
  1. public class TransitionActivity extends AppCompatActivity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(@Nullable Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_transition);  
  7.   
  8.         findViewById(R.id.image).setOnClickListener(new View.OnClickListener() {  
  9.             @Override  
  10.             public void onClick(View view) {  
  11.                 move(view);  
  12.             }  
  13.         });  
  14.     }  
  15.   
  16.     private void move(View view) {  
  17.         Intent intent = new Intent(this, TransitionActivity2.class);  
  18.         final Bundle options = ActivityOptionsCompat  
  19.                 .makeSceneTransitionAnimation(this, view, "image")  
  20.                 .toBundle();  
  21.         startActivity(intent, options);  
  22.     }  
  23. }  




そこで、独自の SharedElement 用 TransitionSet を作ります。

デフォルトはプラットフォームの @transition/move が指定されており、中身は次のようになっています。
  1. <transitionSet xmlns:android="http://schemas.android.com/apk/res/android">  
  2.     <changeBounds/>  
  3.     <changeTransform/>  
  4.     <changeClipBounds/>  
  5.     <changeImageTransform/>  
  6. </transitionSet>  
そこで、以下のようなクラスを用意しました。
  1. public class CustomTransitionSet extends TransitionSet {  
  2.   
  3.     public CustomTransitionSet() {  
  4.         addTransition(new ChangeBounds());  
  5.         addTransition(new ChangeTransform());  
  6.         addTransition(new ChangeClipBounds());  
  7.         addTransition(new ChangeImageTransform());  
  8.         addTransition(new CustomTransition().addTarget(R.id.image2));  
  9.     }  
  10. }  
デフォルトの設定 + CustomTransition を追加しています。 CustomTransition は Activity2 の二つ目の ImageView だけを対象にしたいので、addTarget で対象を絞っています。
この CustomTransitionSet を Activity2 で SharedElement 用の Transition としてセットします。
  1. public class TransitionActivity2 extends AppCompatActivity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(@Nullable Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.   
  7.         getWindow().setSharedElementEnterTransition(new CustomTransitionSet());  
  8.   
  9.         setContentView(R.layout.activity_transition2);  
  10.     }  
  11. }  
CustomTransition では Transition 開始時の view の Visibility を持っておいて、それが VISIBLE 以外だったらフェードアウト、VISIBLE だったらフェードインのアニメーションをするようにしました。
  1. public class CustomTransition extends Transition {  
  2.   
  3.     // TransitionValues に追加するときのキーは パッケージ名:クラス名:プロパティ名  
  4.     private static final String PROP_NAME_VISIBILITY = "net.yanzm.sample:CustomTransition:visibility";  
  5.   
  6.     @Override  
  7.     public void captureStartValues(TransitionValues transitionValues) {  
  8.         // visibility の値を持っておく  
  9.         final View view = transitionValues.view;  
  10.         transitionValues.values.put(PROP_NAME_VISIBILITY, view.getVisibility());  
  11.     }  
  12.   
  13.     @Override  
  14.     public void captureEndValues(TransitionValues transitionValues) {  
  15.         // end の値は使わないので何もしない  
  16.     }  
  17.   
  18.     @Override  
  19.     public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {  
  20.         if (startValues == null || startValues.view == null) {  
  21.             return null;  
  22.         }  
  23.   
  24.         final View view = startValues.view;  
  25.         final int visibility = (int) startValues.values.get(PROP_NAME_VISIBILITY);  
  26.         final boolean isEnter = visibility != View.VISIBLE;  
  27.   
  28.         view.setVisibility(View.VISIBLE);  
  29.         view.setAlpha(isEnter ? 0f : 1f);  
  30.   
  31.         final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha", isEnter ? 1f : 0f);  
  32.         anim.addListener(new AnimatorListenerAdapter() {  
  33.             @Override  
  34.             public void onAnimationEnd(Animator animation) {  
  35.                 view.setAlpha(1f);  
  36.                 view.setVisibility(isEnter ? View.VISIBLE : View.INVISIBLE);  
  37.                 super.onAnimationEnd(animation);  
  38.             }  
  39.         });  
  40.         return anim;  
  41.     }  
  42. }  
これで一番上に載せた動画のような動作になりました!


2016年11月14日月曜日

ViewAnimationUtils.createCircularReveal() を使って FAB の transforming を実現する - with Transition API -

ViewAnimationUtils.createCircularReveal() を使って FAB の transforming を実現する
では、直接Activityに複雑なアニメーションを記述しました。それにより、本質的なコード(toolsContainer と fab の visibility の切り替え)がアニメーションのコードに埋もれてしまい、何をやっているのかわかりずらい状況になっていました。
そこで Transition API を使ってアニメーション部分を Activity から引き剥がしました。

完全な実装は
https://github.com/yanzm/FabTransformingSample
にあります。

MainActivity からは Animator オブジェクトが完全になくなり、RecyclerViewやバックキー部分のコードを追加しても前回より短くなっています。 visibility の切り替えなど view のパラメータ値の変更だけになり、何をやっているのかがわかりやすくなりました。 アニメーション部分は FabTransformation というクラスにまとめています。
  1. public class MainActivity extends AppCompatActivity {  
  2.   
  3.     private static final int HORIZONTAL_FACTOR = 2;  
  4.     private float diff;  
  5.   
  6.     private ViewGroup sceneRoot;  
  7.     private View toolsContainer;  
  8.     private View tools;  
  9.     private FloatingActionButton fab;  
  10.   
  11.     @Override  
  12.     protected void onCreate(Bundle savedInstanceState) {  
  13.         super.onCreate(savedInstanceState);  
  14.         setContentView(R.layout.activity_main);  
  15.   
  16.         toolsContainer = findViewById(R.id.tools_container);  
  17.         tools = findViewById(R.id.tools);  
  18.         fab = (FloatingActionButton) findViewById(R.id.fab);  
  19.   
  20.         sceneRoot = (ViewGroup) findViewById(R.id.scene_root);  
  21.         sceneRoot.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {  
  22.             @Override  
  23.             public void onGlobalLayout() {  
  24.                 int[] toolsLocation = new int[2];  
  25.                 toolsContainer.getLocationInWindow(toolsLocation);  
  26.   
  27.                 int[] fabLocation = new int[2];  
  28.                 fab.getLocationInWindow(fabLocation);  
  29.   
  30.                 diff = (toolsLocation[1] + toolsContainer.getHeight() / 2)  
  31.                         - (fabLocation[1] + fab.getHeight() / 2);  
  32.   
  33.                 final float pivotX = fabLocation[0] + fab.getWidth() / 2 - toolsLocation[0] - diff * HORIZONTAL_FACTOR;  
  34.                 toolsContainer.setPivotX(pivotX);  
  35.                 tools.setPivotX(pivotX);  
  36.   
  37.                 sceneRoot.getViewTreeObserver().removeOnGlobalLayoutListener(this);  
  38.             }  
  39.         });  
  40.   
  41.   
  42.         fab.setOnClickListener(new View.OnClickListener() {  
  43.             @Override  
  44.             public void onClick(View view) {  
  45.                 changeFabMode(truetrue);  
  46.             }  
  47.         });  
  48.   
  49.         changeFabMode(falsefalse);  
  50.   
  51.         // recycler view setup  
  52.         final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);  
  53.         recyclerView.setAdapter(new AndroidVersionAdapter());  
  54.         recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {  
  55.             @Override  
  56.             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {  
  57.                 super.onScrollStateChanged(recyclerView, newState);  
  58.                 if (newState != RecyclerView.SCROLL_STATE_IDLE) {  
  59.                     if (fab.getVisibility() != View.VISIBLE) {  
  60.                         changeFabMode(falsetrue);  
  61.                     }  
  62.                 }  
  63.             }  
  64.         });  
  65.     }  
  66.   
  67.     @Override  
  68.     public void onBackPressed() {  
  69.         if (fab.getVisibility() != View.VISIBLE) {  
  70.             changeFabMode(falsetrue);  
  71.             return;  
  72.         }  
  73.         super.onBackPressed();  
  74.     }  
  75.   
  76.     private void changeFabMode(boolean transformed, boolean animate) {  
  77.         if (animate) {  
  78.             final TransitionSet transition = new FabTransformation(transformed, fab.getHeight() / 2f);  
  79.             TransitionManager.beginDelayedTransition(sceneRoot, transition);  
  80.         }  
  81.   
  82.         final float baseMargin = getResources().getDimension(R.dimen.fab_margin);  
  83.         final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) fab.getLayoutParams();  
  84.         params.bottomMargin = (int) (baseMargin - (transformed ? diff : 0));  
  85.         params.setMarginEnd((int) (baseMargin + (transformed ? diff * HORIZONTAL_FACTOR : 0)));  
  86.         fab.setLayoutParams(params);  
  87.   
  88.         toolsContainer.setVisibility(transformed ? View.VISIBLE : View.INVISIBLE);  
  89.         tools.setVisibility(transformed ? View.VISIBLE : View.INVISIBLE);  
  90.         tools.setScaleX(transformed ? 1f : 0.8f);  
  91.         fab.setVisibility(transformed ? View.INVISIBLE : View.VISIBLE);  
  92.     }  
  93. }  
FabTransformation では複数の Transition を組み合わせて FAB の transforming を実現するアニメーションを構築しています。 ここでは Transition API で用意されている ChangeTransformFadeChangeBounds に加えて、ViewAnimationUtils.createCircularReveal() を利用する CircularRevealTransition を作って利用しています。





ViewAnimationUtils.createCircularReveal() を使って FAB の transforming を実現する

ViewAnimationUtils.createCircularReveal()

ViewAnimationUtils.createCircularReveal() は、Viewを円形にくり抜くアニメーション(Animator)を作るユーティリティメソッドです。 例えばこんな感じ。
  1. public class SampleActivity extends AppCompatActivity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(@Nullable Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_sample);  
  7.   
  8.         findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {  
  9.             @Override  
  10.             public void onClick(View v) {  
  11.                 final View container = findViewById(R.id.container);  
  12.   
  13.                 final int width = container.getWidth();  
  14.                 final int height = container.getHeight();  
  15.                 float startRadius = (float) Math.sqrt(width * width + height * height) / 2;  
  16.                 float endRadius = 0;  
  17.   
  18.                 final Animator animator = ViewAnimationUtils.createCircularReveal(container,  
  19.                         width / 2, height / 2, startRadius, endRadius);  
  20.                 animator.setDuration(3000);  
  21.                 animator.start();  
  22.             }  
  23.         });  
  24.     }  
  25. }  




FAB の transforming

https://material.google.com/components/buttons-floating-action-button.html#buttons-floating-action-button-transitions の真ん中あたり、toolbar という項目のやつです。

アニメーション以外の本質的なコードは toolsContainer と fab の visibility の切り替えだけ(以下の部分)なんですけど、アニメーションのコード入れると長い...
  1. toolsContainer.setVisibility(View.VISIBLE);  
  2. fab.setVisibility(View.INVISIBLE);  
これが全体のコードなのですが、長いですね...
  1. public class MainActivity extends AppCompatActivity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.   
  8.         final View toolsContainer = findViewById(R.id.tools_container);  
  9.         final View tools = findViewById(R.id.tools);  
  10.         final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);  
  11.   
  12.         final ToggleButton toggleButton = (ToggleButton) findViewById(R.id.button);  
  13.         toggleButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {  
  14.             @Override  
  15.             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {  
  16.                 final int fabWidth = fab.getWidth();  
  17.                 final int fabHeight = fab.getHeight();  
  18.   
  19.                 final int toolsWidth = toolsContainer.getWidth();  
  20.                 final int toolsHeight = toolsContainer.getHeight();  
  21.   
  22.                 float startRadius = fabHeight / 2f;  
  23.                 float endRadius = (float) (Math.sqrt(toolsWidth * toolsWidth + toolsHeight * toolsHeight));  
  24.   
  25.                 int[] outLocation = new int[2];  
  26.                 toolsContainer.getLocationInWindow(outLocation);  
  27.   
  28.                 int[] fabOutLocation = new int[2];  
  29.                 fab.getLocationInWindow(fabOutLocation);  
  30.   
  31.                 float diff = isChecked  
  32.                         ? (outLocation[1] + toolsHeight / 2) - (fabOutLocation[1] + fabHeight / 2)  
  33.                         : 0;  
  34.   
  35.                 int centerX = (int) (fabOutLocation[0] + fabWidth / 2 - outLocation[0] - diff);  
  36.                 int centerY = toolsHeight / 2;  
  37.   
  38.                 final int FAB_DURATION = 100;  
  39.                 final int TOOLS_DURATION = 300;  
  40.   
  41.                 if (isChecked) {  
  42.                     final Animator fabAnimator1 = ObjectAnimator.ofFloat(fab, "translationY", diff);  
  43.                     fabAnimator1.setDuration(FAB_DURATION);  
  44.                     fabAnimator1.setInterpolator(new DecelerateInterpolator());  
  45.   
  46.                     final Animator fabAnimator2 = ObjectAnimator.ofFloat(fab, "translationX", -diff);  
  47.                     fabAnimator2.setDuration(FAB_DURATION);  
  48.                     fabAnimator2.setInterpolator(new AccelerateInterpolator());  
  49.   
  50.                     final ValueAnimator fabAnimator3 = ValueAnimator.ofInt(2550);  
  51.                     fabAnimator3.setDuration(FAB_DURATION);  
  52.                     fabAnimator3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
  53.                         @Override  
  54.                         public void onAnimationUpdate(ValueAnimator animation) {  
  55.                             final int alpha = (int) animation.getAnimatedValue();  
  56.                             final Drawable drawable = fab.getDrawable();  
  57.                             drawable.setAlpha(alpha);  
  58.                         }  
  59.                     });  
  60.   
  61.                     final Animator toolsContainerAnimator = ViewAnimationUtils.createCircularReveal(toolsContainer,  
  62.                             centerX, centerY, startRadius, endRadius);  
  63.                     toolsContainerAnimator.setDuration(TOOLS_DURATION);  
  64.                     toolsContainerAnimator.addListener(new AnimatorListenerAdapter() {  
  65.                         @Override  
  66.                         public void onAnimationStart(Animator animation) {  
  67.                             super.onAnimationStart(animation);  
  68.                             toolsContainer.setVisibility(View.VISIBLE);  
  69.                             fab.setVisibility(View.INVISIBLE);  
  70.                         }  
  71.                     });  
  72.   
  73.                     tools.setPivotX(centerX);  
  74.                     final Animator toolsAnimator = ObjectAnimator.ofPropertyValuesHolder(tools,  
  75.                             PropertyValuesHolder.ofFloat("alpha", 0f, 1f),  
  76.                             PropertyValuesHolder.ofFloat("scaleX"0.8f, 1f));  
  77.                     toolsAnimator.setDuration(TOOLS_DURATION);  
  78.   
  79.                     AnimatorSet set = new AnimatorSet();  
  80.                     set.play(toolsContainerAnimator).with(toolsAnimator)  
  81.                             .after(fabAnimator1).after(fabAnimator2).after(fabAnimator3);  
  82.                     set.start();  
  83.   
  84.                 } else {  
  85.                     final Animator fabAnimator1 = ObjectAnimator.ofFloat(fab, "translationY"0);  
  86.                     fabAnimator1.setDuration(FAB_DURATION);  
  87.                     fabAnimator1.setInterpolator(new AccelerateInterpolator());  
  88.   
  89.                     final Animator fabAnimator2 = ObjectAnimator.ofFloat(fab, "translationX"0);  
  90.                     fabAnimator2.setDuration(FAB_DURATION);  
  91.                     fabAnimator2.setInterpolator(new DecelerateInterpolator());  
  92.   
  93.                     final ValueAnimator fabAnimator3 = ValueAnimator.ofInt(0255);  
  94.                     fabAnimator3.setDuration(FAB_DURATION);  
  95.                     fabAnimator3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
  96.                         @Override  
  97.                         public void onAnimationUpdate(ValueAnimator animation) {  
  98.                             final int alpha = (int) animation.getAnimatedValue();  
  99.                             final Drawable drawable = fab.getDrawable();  
  100.                             drawable.setAlpha(alpha);  
  101.                         }  
  102.                     });  
  103.   
  104.                     final Animator toolsContainerAnimator = ViewAnimationUtils.createCircularReveal(  
  105.                             toolsContainer, centerX, centerY, endRadius, startRadius);  
  106.                     toolsContainerAnimator.setDuration(TOOLS_DURATION);  
  107.                     toolsContainerAnimator.addListener(new AnimatorListenerAdapter() {  
  108.                         @Override  
  109.                         public void onAnimationEnd(Animator animation) {  
  110.                             super.onAnimationEnd(animation);  
  111.                             toolsContainer.setVisibility(View.INVISIBLE);  
  112.                             fab.setVisibility(View.VISIBLE);  
  113.                         }  
  114.                     });  
  115.   
  116.                     tools.setPivotX(centerX);  
  117.                     final Animator toolsAnimator = ObjectAnimator.ofPropertyValuesHolder(tools,  
  118.                             PropertyValuesHolder.ofFloat("alpha", 0f),  
  119.                             PropertyValuesHolder.ofFloat("scaleX"0.8f));  
  120.                     toolsAnimator.setDuration(TOOLS_DURATION);  
  121.   
  122.                     AnimatorSet set = new AnimatorSet();  
  123.                     set.play(toolsContainerAnimator).with(toolsAnimator)  
  124.                             .before(fabAnimator1).before(fabAnimator2).before(fabAnimator3);  
  125.                     set.start();  
  126.                 }  
  127.             }  
  128.         });  
  129.   
  130.         toolsContainer.setVisibility(toggleButton.isChecked() ? View.VISIBLE : View.INVISIBLE);  
  131.         tools.setAlpha(toggleButton.isChecked() ? 1f : 0f);  
  132.     }  
  133. }  
アニメーションの長いコードがあるために、ここでやっていること(つまり toolsContainer と fab の visibility を切り替えること)がわかりにくくなっています。
それを解消するために Transition API が使えます(Transition API はもともとそういうための用意されたもののようです)。それは次回に。

↓実行結果



2016年11月4日金曜日

RecyclerView のクリックをどこで処理するか

RecyclerView.Adapter の責務はデータと ViewHolder の紐付けなので、View がタップされたときの処理(リスナーをセットすることではない)を Adapter 内に書くべきではないと思っています。

ではどうしているかというと、View がタップされたときに呼び出すメソッドを Adapter 内に定義しておき、Activity や Fragment で Adapter を生成するときにそのメソッドを Override してタップされたときの処理を記述するようにしています。
  1. public class MainActivity extends AppCompatActivity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.   
  8.         final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);  
  9.   
  10.         final VersionAdapter adapter = new VersionAdapter() {  
  11.             @Override  
  12.             protected void onVersionClicked(@NonNull String version) {  
  13.                 super.onVersionClicked(version);  
  14.                 // Activity 側でタップされたときの処理を行う  
  15.                 Toast.makeText(MainActivity.this, version, Toast.LENGTH_SHORT).show();  
  16.             }  
  17.         };  
  18.   
  19.         recyclerView.setAdapter(adapter);  
  20.     }  
  21.   
  22.     public static class VersionAdapter extends RecyclerView.Adapter<VersionViewHolder> {  
  23.   
  24.         // タップされたときに呼び出されるメソッドを定義  
  25.         protected void onVersionClicked(@NonNull String version) {  
  26.         }  
  27.   
  28.         private final List<String> versions = new ArrayList<>();  
  29.   
  30.         @Override  
  31.         public VersionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {  
  32.             final LayoutInflater inflater = LayoutInflater.from(parent.getContext());  
  33.             final VersionViewHolder holder = VersionViewHolder.create(inflater, parent);  
  34.             // onCreateViewHolder でリスナーをセット  
  35.             holder.itemView.setOnClickListener(new View.OnClickListener() {  
  36.                 @Override  
  37.                 public void onClick(View v) {  
  38.                     final int position = holder.getAdapterPosition();  
  39.                     final String version = versions.get(position);  
  40.                     onVersionClicked(version);  
  41.                 }  
  42.             });  
  43.             return holder;  
  44.         }  
  45.   
  46.         ...  
  47.     }  
  48. }  
完全なサンプルは
https://github.com/yanzm/RecyclerViewSample
にあります。