ぐぐると結構出てくるので案外簡単だろうと思ったら地雷を踏みぬきまくったので覚書。
主にやることは以下の二つ
- 開閉を表すアイコンをクルッとする
- 要素の開閉時にせり出てくるようにする
基本的には参考サイトに上げてある内容の焼きまし+αになってるので、そちらを読んでいただいてもOK。
特に序盤はほぼ内容がかぶっているので、「RecycleViewに適応する」あたりから読んでもOKです。
アイコンクルっと編
基本的にはアイコンのViewに対してRotateAnimationを適応すれば良い
例
public void rotate(ImageView iconView, long duration) {
int pivotX = iconView.getWidth() / 2;
int pivotY = iconView.getHeight() / 2;
RotateAnimation rotate = new RotateAnimation(0 , 180, pivotX, pivotY);
rotate.setDuration(duration);
rotate.setFillAfter(true);
iconView.startAnimation(rotate);
}
pivotXとpivotYで回転の中心を決める。アイコンのwidthとheightの2分の1なので、画像の重心を中心として回転する
new RotateAnimationの引数。1つ目が回転の始点で、何度から回転するか。0なので0度(回転なしの状態)からスタート。
2つ目が回転の終点。時計回りに180度回転したところで止まる
setDurationはそのまんまで何秒かけて回転するか。単位はミリ秒
setFillAfterはアニメーションした後、その状態を保持するかどうか。RotateAnimationの場合なら、回転した後で止める(180度ひっくり返った状態)か、元の状態(0度の状態)かになる。
閉じるときに逆回転するには?
開くときに「v」→「>」→「^」というアニメーションにしたなら、閉じるときはその逆をたどりたい。つまり「^」→「>」→「v」というアニメーションにしたい場合。
new RotateAnimation(180, 0 , pivotX, pivotY);
始点を180に、終点を0にすれば逆回転する。
##開くときに反時計回りでアイコンを回転させるには?
new RotateAnimation(0, -180, pivotX, pivotY);
ちなみに閉じるときの場合は
new RotateAnimation(-180, 0, pivotX, pivotY);
要素を開閉(Expand/Collapse)する編
今回はViewの中に、開閉するLinierLayoutがあり、それの高さを変える、という手法を紹介(これが一番シンプル?)
ResizeAnimationを作る
そもそもにAndroidにはリサイズ用のアニメーションがデフォルトで用意されていないため、自作する必要がある.が、参考サイトにあがっているResizeAnimation.javaの内容をコピペすれば事足りるのでここでは割愛
#RecycleViewに適応する
じつはここに落とし穴がいっぱい潜んでいるので注意が必要。
実装例は以下のような感じ
public class MyRecycleViewAdapter extends RecycleView.Adapter<MyAdapter.ViewHolder> {
private List<SomeEntity> mEntities;
private List<String> mExpandedKeys = new ArrayList();
//コンストラクタやら、overrideの実装は割愛
@override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.onBindViewHolder(mEntities.get(position));
}
class ViewHolder extends RecycleView.ViewHolder {
View mView; //リストの要素全体
ImageView mRotateIcon; //回転するアイコン
LinerLayout mDetailContents; //開閉するコンテンツ部分のレイアウト
int final originalHeight; //開閉するコンテンツの、開いたときの高さ
public ViewHolder(View view) {
mView = view;
originalHeight = mView.getContext().getResources().getDimension(R.dimen.detail_height);
}
void onBindViewHolder(final SomeEntity entity) {
if(mExpandedKeys.contains(entity.getKey()) {
//コンテンツが開かれている場合
mDetailContents.getLayoutParams().height = originalHeight;
rotateExpandAnimation(mIconView, 0); //開くときの回転アニメーションをduration=0で呼ぶ
} else {
//コンテンツが閉じられている場合
mDetailContents.getLayoutParams().height = 0;
}
mView.setOnClickListener (view -> {
if(mExpandedKeys.contains(entity.getKey()) {
rotateCollapseAnimation(mIconView, 300);
collapseAnimation(mDetailLayout, 300);
//Collapseアニメーションを呼ぶ
mExpandedKeys.remove(entity.getKey())
} else {
rotateExpandAnimation(mIconView ,300);
expandAnimation(mDetailLayout, 300);
mExpandedKeys.add(entity.getKey());
}
});
}
}
}
注意点
entity.getKey()でSomeEntityを一意に定めるキーを取得してる想定。ハッシュキーとかで実装してください。
XXXAnimation系の第1引数はアニメーションする対象のView,第二引数はアニメーションのdurationを表す。
上記の例ではAnimation系のメソッドは記載してないので、privateメソッドを作るなりせんようのUtilityクラスを作るなりで自由に実装してください。
#トラブルシューティング
すでに一部出来ているから、必要なところだけ取り込む→うごかねぇ、というパターンの人に。
ちなみに私は全て踏み抜きました。
##ResizeAnimationに渡すheightが0になる
上記の例でいうなら、
originalHeight = mDetailContents.LayoutParams().height;
とかやってる場合。コンテンツが閉じられている状態なら、高さは0になるので正常に動かない。一番早い方法は決め打ちにする方法。(動的に高さを決める方法は自分も知りたい)
##端末(解像度)によって高さが大きく変わる
上記の例で言うなら
originalHeight = 600;
とかやってる場合。ここでの単位はPixelになっているので、端末によって変わってきてしまう。なので、dpで定義する必要がある。
一番簡単な方法はリソースのdimen.xmlに値を記載し、それをgetResouceで取る方法。上記の例で言うと、以下の部分。
int final originalHeight = mView.getContext().getResources().getDimension(R.dimen.detail_height);
##要素を開いた後、スクロールして戻ってきたら開閉アイコンが元に戻っている
原因箇所は以下の、**rotateExpandAnimation(mIconView, 0);**の部分。
if(mExpandedKeys.contains(final entity.getKey()) {
//コンテンツが開かれている場合
mDetailContents.getLayoutParams().height = originalHeight;
rotateExpandAnimation(mIconView, 0); //開くときの回転アニメーションをduration=0で呼ぶ
} else {
//コンテンツが閉じられている場合
mDetailContents.getLayoutParams().height = 0;
}
回転アニメーションを呼び出していないのが原因。
##アイコンの向きは正しいのに、タップしたらいきなり逆向きになった
上記の類似問題。回転処理を呼び出してはいるが、別の方法になっている可能性が高い
ありがちなパターンは、以下の三つ。
###1つ目。
mIconView.setRotation(180); //ViewがもってるsetRotationで回転させる
調べたわけではないが、RotateAnimationで回転させた状態と、setRotationで回転させたものは別扱いになっている模様。
なので、回転させたつもりでも、RotateAnimation側では回転してないことになってる
###2つ目
mIconView.setResouse(R.drawable.collapse_icon); //回転した状態のアイコンをセットしている
内部では回転していないことになっているので、いきなり逆向きになる。
###3つ目
mIconView.setVisibility(View.GONE); //開くアイコンを非表示にし、開かれたアイコンを表示する
mExpandedIconView.setVisibility(View.VISIBLE);
そもそもリソースが異なるので回転状態が維持されてない。
うまく表示/非表示を切り替えれば問題なくなるかもしれないが、実装が汚くなるし、ちらつきの原因になりかねないのでオススメしない。
##スクロールさせたら開いてないはずの要素が開いてる
やりがちなミスナンバーワン。
ViewHolderで開閉状態を管理してしまっていることが原因。例えば以下のような感じ。
class ViewHolder extends /*略*/ {
boolean isExpanded;
void onBindViewHolder (final SomeEntity entity) {
if (isExpanded) {
//開いているときの処理
} else {
//閉じているときの処理
}
mView.onClickListener(view -> {
if (isExpanded) {
isExpanded = false;
else {
isExpanded = true;
}
});
}
}
ViewHolderはあくまでViewの入れ物なので、使い回されることが前提になっている。
なので、そこで状態を管理しようとするとそのまま引き継がれてしまう。
対処としては、ViewHolder外で状態を管理すること。例ではmExpandedKeysのリストで管理してる。
##アニメーションが動かない
原因不明度ナンバーワン。開閉はされるし、アイコンの向きも変わるのにアニメーションが動かない。
原因は状態の管理方法にある。例えば以下のような感じ。
class ViewHolder extends /*略*/ {
void onBindViewHolder(final SomeEntity entity) {
if (entity.isExpanded()) {
//開いているときの処理
} else {
//閉じているときの処理
}
mView.onClickListener(view -> {
if (entity.isExpanded()) {
entity.setIsExpanded(false);
else {
entity.setIsExpanded(true);
}
});
}
}
一見すると何も問題なさそうに見えるのが曲者。
原因はentityに対して、値をsetしていること。
これによってentityの状態が変わるので再描画が走り、アニメーションが走らずに、変わった結果が反映されてしまう、ということになる模様。
なので、状態を管理する場合はentityとは別で管理する必要がある。