Facebookアプリには、投稿上の小さな画像と、ユーザーがズームできる拡大モードとの間の素敵な遷移アニメーションがあります。
私が見ているように、アニメーションは前の場所とサイズに従ってimageViewを拡大および移動するだけでなく、imageViewのコンテンツを引き伸ばす代わりにコンテンツを明らかにします。
これは、私が作成した次のスケッチを使用して確認できます。
彼らはどのようにそれをしましたか?彼らは本当にコンテンツを明らかにするためにアニメーション化する2つのビューを持っていましたか?
どうやってそれを単一のビューのように流動的にしたのですか?
全画面に拡大された画像の私が見た唯一のチュートリアル(リンク ここ )はうまく表示されませんサムネイルが中央トリミングに設定されている場合。
それだけでなく、Androidの低APIでも動作します。
同様の能力を持つ図書館を知っている人はいますか?
編集:私は方法を見つけて 回答を投稿しました 、しかしそれはlayoutParamsの変更に基づいています、そして私はそれが効率的ではなく、推奨されません。
通常のアニメーションやその他のアニメーションのトリックを使用してみましたが、今のところ、それだけがうまくいきました。
誰かがそれをより良い方法で機能させるために何をすべきかを知っているなら、それを書き留めてください。
わかりました、私はそれを行うための可能な方法を見つけました。 nineOldAndroidsライブラリ のObjectAnimatorを使用して、変化し続ける変数としてlayoutParamsを作成しました。 onDrawとonLayoutが多く発生するため、これを実現するのに最適な方法ではないと思いますが、コンテナーのビューが少なく、サイズが変更されない場合は、問題ない可能性があります。
アニメーション化するimageViewは、最終的に必要な正確なサイズを取り、(現在)サムネイルとアニメーション化されたimageViewの両方が同じコンテナを持っていることを前提としています(ただし、簡単に変更できるはずです。
私がテストしたように、 TouchImageView クラスを拡張することでズーム機能を追加することも可能です。最初にスケールタイプを中央クロップに設定し、アニメーションが終了したらマトリックスに戻します。必要に応じて、layoutParamsを設定してコンテナ全体を埋めることができます(マージンを0,0に設定します)。 )。
また、どうしてAnimatorSetが機能しなかったのか疑問に思うので、誰かが私に何をすべきか教えてくれることを期待して、ここで機能するものを示します。
コードは次のとおりです。
public class MainActivity extends Activity {
private static final int IMAGE_RES_ID = R.drawable.test_image_res_id;
private static final int ANIM_DURATION = 5000;
private final Handler mHandler = new Handler();
private ImageView mThumbnailImageView;
private CustomImageView mFullImageView;
private Point mFitSizeBitmap;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mFullImageView = (CustomImageView) findViewById(R.id.fullImageView);
mThumbnailImageView = (ImageView) findViewById(R.id.thumbnailImageView);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
prepareAndStartAnimation();
}
}, 2000);
}
private void prepareAndStartAnimation() {
final int thumbX = mThumbnailImageView.getLeft(), thumbY = mThumbnailImageView.getTop();
final int thumbWidth = mThumbnailImageView.getWidth(), thumbHeight = mThumbnailImageView.getHeight();
final View container = (View) mFullImageView.getParent();
final int containerWidth = container.getWidth(), containerHeight = container.getHeight();
final Options bitmapOptions = getBitmapOptions(getResources(), IMAGE_RES_ID);
mFitSizeBitmap = getFitSize(bitmapOptions.outWidth, bitmapOptions.outHeight, containerWidth, containerHeight);
mThumbnailImageView.setVisibility(View.GONE);
mFullImageView.setVisibility(View.VISIBLE);
mFullImageView.setContentWidth(thumbWidth);
mFullImageView.setContentHeight(thumbHeight);
mFullImageView.setContentX(thumbX);
mFullImageView.setContentY(thumbY);
runEnterAnimation(containerWidth, containerHeight);
}
private Point getFitSize(final int width, final int height, final int containerWidth, final int containerHeight) {
int resultHeight, resultWidth;
resultHeight = height * containerWidth / width;
if (resultHeight <= containerHeight) {
resultWidth = containerWidth;
} else {
resultWidth = width * containerHeight / height;
resultHeight = containerHeight;
}
return new Point(resultWidth, resultHeight);
}
public void runEnterAnimation(final int containerWidth, final int containerHeight) {
final ObjectAnimator widthAnim = ObjectAnimator.ofInt(mFullImageView, "contentWidth", mFitSizeBitmap.x)
.setDuration(ANIM_DURATION);
final ObjectAnimator heightAnim = ObjectAnimator.ofInt(mFullImageView, "contentHeight", mFitSizeBitmap.y)
.setDuration(ANIM_DURATION);
final ObjectAnimator xAnim = ObjectAnimator.ofInt(mFullImageView, "contentX",
(containerWidth - mFitSizeBitmap.x) / 2).setDuration(ANIM_DURATION);
final ObjectAnimator yAnim = ObjectAnimator.ofInt(mFullImageView, "contentY",
(containerHeight - mFitSizeBitmap.y) / 2).setDuration(ANIM_DURATION);
widthAnim.start();
heightAnim.start();
xAnim.start();
yAnim.start();
// TODO check why using AnimatorSet doesn't work here:
// final com.nineoldandroids.animation.AnimatorSet set = new AnimatorSet();
// set.playTogether(widthAnim, heightAnim, xAnim, yAnim);
}
public static BitmapFactory.Options getBitmapOptions(final Resources res, final int resId) {
final BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, bitmapOptions);
return bitmapOptions;
}
}
<RelativeLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:tools="http://schemas.Android.com/tools"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
tools:context=".MainActivity" >
<com.example.facebookstylepictureanimationtest.CustomImageView
Android:id="@+id/fullImageView"
Android:layout_width="0px"
Android:layout_height="0px"
Android:background="#33ff0000"
Android:scaleType="centerCrop"
Android:src="@drawable/test_image_res_id"
Android:visibility="invisible" />
<ImageView
Android:id="@+id/thumbnailImageView"
Android:layout_width="100dp"
Android:layout_height="100dp"
Android:layout_alignParentBottom="true"
Android:layout_alignParentRight="true"
Android:scaleType="centerCrop"
Android:src="@drawable/test_image_res_id" />
</RelativeLayout>
public class CustomImageView extends ImageView {
public CustomImageView(final Context context) {
super(context);
}
public CustomImageView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public CustomImageView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
}
public void setContentHeight(final int contentHeight) {
final LayoutParams layoutParams = getLayoutParams();
layoutParams.height = contentHeight;
setLayoutParams(layoutParams);
}
public void setContentWidth(final int contentWidth) {
final LayoutParams layoutParams = getLayoutParams();
layoutParams.width = contentWidth;
setLayoutParams(layoutParams);
}
public int getContentHeight() {
return getLayoutParams().height;
}
public int getContentWidth() {
return getLayoutParams().width;
}
public int getContentX() {
return ((MarginLayoutParams) getLayoutParams()).leftMargin;
}
public void setContentX(final int contentX) {
final MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = contentX;
setLayoutParams(layoutParams);
}
public int getContentY() {
return ((MarginLayoutParams) getLayoutParams()).topMargin;
}
public void setContentY(final int contentY) {
final MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
layoutParams.topMargin = contentY;
setLayoutParams(layoutParams);
}
}
別の解決策として、画像のアニメーションを小さいものから大きいものまで作成したい場合は、ActivityOptions.makeThumbnailScaleUpAnimationまたはmakeScaleUpAnimationを試して、それらが適切かどうかを確認できます。
これはTransition
Apiを介して実現でき、これは結果gifです。
以下の重要なコード:
private void zoomIn() {
ViewGroup.LayoutParams layoutParams = mImage.getLayoutParams();
int width = layoutParams.width;
int height = layoutParams.height;
layoutParams.width = (int) (width * 2);
layoutParams.height = height * 2;
mImage.setLayoutParams(layoutParams);
mImage.setScaleType(ImageView.ScaleType.FIT_CENTER);
TransitionSet transitionSet = new TransitionSet();
Transition bound = new ChangeBounds();
transitionSet.addTransition(bound);
Transition changeImageTransform = new ChangeImageTransform();
transitionSet.addTransition(changeImageTransform);
transitionSet.setDuration(1000);
TransitionManager.beginDelayedTransition(mRootView, transitionSet);
}
sDKバージョン> = 21
Github でサンプルコードを作成しました。
このコードの鍵は、canvas.clipRect()
を使用することです。ただし、CroppedImageview
がmatch_parent
の場合にのみ機能します。
簡単に説明すると、スケールと翻訳のアニメーションはViewPropertyAnimator
に任せます。次に、画像のトリミングに集中できます。
上の画像のように、クリッピング領域を計算し、クリップ領域を最終的なビューサイズに変更します。
class ZoomAnimationController(private val view: CroppedImageView, startRect: Rect, private val viewRect: Rect, imageSize: Size) {
companion object {
const val DURATION = 300L
}
private val startViewRect: RectF
private val scale: Float
private val startClipRect: RectF
private val animatingRect: Rect
private var cropAnimation: ValueAnimator? = null
init {
val startImageRect = getProportionalRect(startRect, imageSize, ImageView.ScaleType.CENTER_CROP)
startViewRect = getProportionalRect(startImageRect, viewRect.getSize(), ImageView.ScaleType.CENTER_CROP)
scale = startViewRect.width() / viewRect.width()
val finalImageRect = getProportionalRect(viewRect, imageSize, ImageView.ScaleType.FIT_CENTER)
startClipRect = getProportionalRect(finalImageRect, startRect.getSize() / scale, ImageView.ScaleType.FIT_CENTER)
animatingRect = Rect()
startClipRect.round(animatingRect)
}
fun init() {
view.x = startViewRect.left
view.y = startViewRect.top
view.pivotX = 0f
view.pivotY = 0f
view.scaleX = scale
view.scaleY = scale
view.setClipRegion(animatingRect)
}
fun startAnimation() {
cropAnimation = createCropAnimator().apply {
start()
}
view.animate()
.x(0f)
.y(0f)
.scaleX(1f)
.scaleY(1f)
.setDuration(DURATION)
.start()
}
private fun createCropAnimator(): ValueAnimator {
return ValueAnimator.ofFloat(0f, 1f).apply {
duration = DURATION
addUpdateListener {
val weight = animatedValue as Float
animatingRect.set(
(startClipRect.left * (1 - weight) + viewRect.left * weight).toInt(),
(startClipRect.top * (1 - weight) + viewRect.top * weight).toInt(),
(startClipRect.right * (1 - weight) + viewRect.right * weight).toInt(),
(startClipRect.bottom * (1 - weight) + viewRect.bottom * weight).toInt()
)
Log.d("SSO", "animatingRect=$animatingRect")
view.setClipRegion(animatingRect)
}
}
}
private fun getProportionalRect(viewRect: Rect, imageSize: Size, scaleType: ImageView.ScaleType): RectF {
return getProportionalRect(RectF(viewRect), imageSize, scaleType)
}
private fun getProportionalRect(viewRect: RectF, imageSize: Size, scaleType: ImageView.ScaleType): RectF {
val viewRatio = viewRect.height() / viewRect.width()
if ((scaleType == ImageView.ScaleType.FIT_CENTER && viewRatio > imageSize.ratio)
|| (scaleType == ImageView.ScaleType.CENTER_CROP && viewRatio <= imageSize.ratio)) {
val width = viewRect.width()
val height = width * imageSize.ratio
val paddingY = (height - viewRect.height()) / 2f
return RectF(viewRect.left, viewRect.top - paddingY, viewRect.right, viewRect.bottom + paddingY)
} else if ((scaleType == ImageView.ScaleType.FIT_CENTER && viewRatio <= imageSize.ratio)
|| (scaleType == ImageView.ScaleType.CENTER_CROP && viewRatio > imageSize.ratio)){
val height = viewRect.height()
val width = height / imageSize.ratio
val paddingX = (width - viewRect.width()) / 2f
return RectF(viewRect.left - paddingX, viewRect.top, viewRect.right + paddingX, viewRect.bottom)
}
return RectF()
}
override fun onDraw(canvas: Canvas?) {
if (clipRect.width() > 0 && clipRect.height() > 0) {
canvas?.clipRect(clipRect)
}
super.onDraw(canvas)
}
fun setClipRegion(rect: Rect) {
clipRect.set(rect)
invalidate()
}
croppedImageviewがmatch_parentの場合にのみ機能します。これは、
なぜみんながフレームワークについて話しているのかわかりません。他の人々のコードを使用することは時々素晴らしいことがあります。しかし、あなたが求めているのは見た目を正確に制御しているように聞こえます。グラフィックスコンテキストにアクセスすることで、それを実現できます。グラフィックスコンテキストがある環境では、タスクは非常に簡単です。 Androidでは、onDraw
メソッドをオーバーライドし、Canvas Objectを使用することで取得できます。さまざまな縮尺、位置、切り抜きで画像を描画するために必要なものがすべて揃っています。そのタイプのものに精通している場合は、マトリックスを使用することもできます。
ステップ
配置、スケール、およびクリップを正確に制御できることを確認してください。これは、オブジェクトコンテナ内で設定される可能性のあるレイアウトまたは自動調整を無効にすることを意味します。
線形補間のパラメーターt
が何であるか、およびそれを時間にどのように関連付けるかを理解します。どのくらい速くまたは遅く、そして緩和がありますか。 t
は時間に依存する必要があります。
サムネイルがキャッシュされたら、フルスケールの画像をバックグラウンドで読み込みます。ただし、まだ表示しないでください。
アニメーショントリガーが起動したら、大きな画像を表示し、初期プロパティの状態から最終プロパティの状態への補間を使用して、t
パラメーターでアニメーションを駆動します。これは、位置、スケール、クリップの3つのプロパティすべてに対して行います。したがって、すべてのプロパティに対して次のことを行います。
Sinterpolated = Sinitial * (t-1) + Sfinal * t;
// where
// t is between 0.0 and 1.0
// and S is the states value
// for every part of scale, position, and clip
//
// Sinitial is what you are going from
// Sfinal is what you are going to
//
// t should change from 0.0->1.0 in
// over time anywhere from 12/sec or 60/sec.
すべてのプロパティが同じパラメータによって駆動される場合、アニメーションはスムーズになります。追加のボーナスとして、ここにタイミングのヒントがあります。 t
パラメータを0から1の間に保つことができる限り、イージングインまたはイージングアウトは1行のコードでハッキングできます。
// After your t is all setup
t = t * t; // for easing in
// or
t = Math.sqrt(t); // for easing out
最も簡単な方法は、scaleTypeをcenterCropに維持しながら、ImageView(通常のimageview、カスタムビューは不要)の高さをアニメーション化することだと思います。これは、で画像の高さをwrap_contentに設定すると事前に知ることができます。レイアウトしてから、ViewTreeObserverを使用してレイアウトがいつ終了したかを確認します。これにより、ImageViewの高さを取得して、新しい「折りたたまれた」高さを設定できます。私はそれをテストしていませんが、これは私がそれを行う方法です。
この投稿もご覧ください。似たようなことをしています http://nerds.airbnb.com/Host-experience-Android/
簡単なプロトタイプで同様の効果を得る方法を見つけました。実稼働での使用には適していない可能性がありますが(まだ調査中です)、すばやく簡単に実行できます。
アクティビティ/フラグメントトランジションでフェードトランジションを使用します(これは、ImageViewがまったく同じ位置にあることから始まります)。フラグメントバージョン:
final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
...etc
アクティビティバージョン:
Intent intent = new Intent(context, MyDetailActivity.class);
startActivity(intent);
getActivity().overridePendingTransition(Android.R.anim.fade_in, Android.R.anim.fade_out);
これにより、ちらつきのないスムーズな移行が可能になります。
新しいフラグメントのonStart()でレイアウトを動的に調整します(メンバーフィールドをonCreateViewのUIの適切な部分に保存し、このコードが1回だけ呼び出されるようにフラグを追加する必要があります)。
@Override
public void onStart() {
super.onStart();
// Remove the padding on any layouts that the image view is inside
mMainLayout.setPadding(0, 0, 0, 0);
// Get the screen size using a utility method, e.g.
// http://stackoverflow.com/a/12082061/112705
// then work out your desired height, e.g. using the image aspect ratio.
int desiredHeight = (int) (screenWidth * imgAspectRatio);
// Resize the image to fill the whole screen width, removing
// any layout margins that it might have (you may need to remove
// padding too)
LinearLayout.LayoutParams layoutParams =
new LinearLayout.LayoutParams(screenWidth, desiredHeight);
layoutParams.setMargins(0, 0, 0, 0);
mImageView.setLayoutParams(layoutParams);
}