/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.constraintlayout.helper.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.constraintlayout.motion.widget.Debug;
import androidx.constraintlayout.motion.widget.Key;
import androidx.constraintlayout.motion.widget.KeyAttributes;
import androidx.constraintlayout.motion.widget.KeyPosition;
import androidx.constraintlayout.motion.widget.MotionController;
import androidx.constraintlayout.motion.widget.MotionHelper;
import androidx.constraintlayout.motion.widget.MotionLayout;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.R;
import java.util.HashMap;
/**
* MotionHelper that automatically inserts keyframes for views moving in a given
* direction, out of:
* <ul>
* <li>NORTH</li>
* <li>SOUTH</li>
* <li>EAST</li>
* <li>WEST</li>
* </ul>
*
* By default, will pick the opposite of the dominant direction (e.g. elements /not/ moving
* in the dominant direction will have the keyframes inserted).
*/
public class MotionEffect extends MotionHelper {
public static final String TAG = "FadeMove";
public static final int AUTO = -1;
public static final int NORTH = 0;
public static final int SOUTH = 1;
public static final int EAST = 2;
public static final int WEST = 3;
private float mMotionEffectAlpha = 0.1f;
private int mMotionEffectStart = 49;
private int mMotionEffectEnd = 50;
private int mMotionEffectTranslationX = 0;
private int mMotionEffectTranslationY = 0;
private boolean mMotionEffectStrictMove = true;
private static final int UNSET = -1;
private int mViewTransitionId = UNSET;
private int mFadeMove = AUTO;
public MotionEffect(Context context) {
super(context);
}
public MotionEffect(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public MotionEffect(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MotionEffect);
final int n = a.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
if (attr == R.styleable.MotionEffect_motionEffect_start) {
mMotionEffectStart = a.getInt(attr, mMotionEffectStart);
mMotionEffectStart = Math.max(Math.min(mMotionEffectStart, 99), 0);
} else if (attr == R.styleable.MotionEffect_motionEffect_end) {
mMotionEffectEnd = a.getInt(attr, mMotionEffectEnd);
mMotionEffectEnd = Math.max(Math.min(mMotionEffectEnd, 99), 0);
} else if (attr == R.styleable.MotionEffect_motionEffect_translationX) {
mMotionEffectTranslationX =
a.getDimensionPixelOffset(attr, mMotionEffectTranslationX);
} else if (attr == R.styleable.MotionEffect_motionEffect_translationY) {
mMotionEffectTranslationY =
a.getDimensionPixelOffset(attr, mMotionEffectTranslationY);
} else if (attr == R.styleable.MotionEffect_motionEffect_alpha) {
mMotionEffectAlpha = a.getFloat(attr, mMotionEffectAlpha);
} else if (attr == R.styleable.MotionEffect_motionEffect_move) {
mFadeMove = a.getInt(attr, mFadeMove);
} else if (attr == R.styleable.MotionEffect_motionEffect_strict) {
mMotionEffectStrictMove = a.getBoolean(attr, mMotionEffectStrictMove);
} else if (attr == R.styleable.MotionEffect_motionEffect_viewTransition) {
mViewTransitionId = a.getResourceId(attr, mViewTransitionId);
}
}
if (mMotionEffectStart == mMotionEffectEnd) {
if (mMotionEffectStart > 0) {
mMotionEffectStart--;
} else {
mMotionEffectEnd++;
}
}
a.recycle();
}
}
@Override
public boolean isDecorator() {
return true;
}
@Override
public void onPreSetup(MotionLayout motionLayout,
HashMap<View,
MotionController> controllerMap) {
View[] views = getViews((ConstraintLayout) this.getParent());
if (views == null) {
Log.v(TAG, Debug.getLoc() + " views = null");
return;
}
// Prepare a set of keyframes to be inserted
KeyAttributes alpha1 = new KeyAttributes();
KeyAttributes alpha2 = new KeyAttributes();
alpha1.setValue(Key.ALPHA, mMotionEffectAlpha);
alpha2.setValue(Key.ALPHA, mMotionEffectAlpha);
alpha1.setFramePosition(mMotionEffectStart);
alpha2.setFramePosition(mMotionEffectEnd);
KeyPosition stick1 = new KeyPosition();
stick1.setFramePosition(mMotionEffectStart);
stick1.setType(KeyPosition.TYPE_CARTESIAN);
stick1.setValue(KeyPosition.PERCENT_X, 0);
stick1.setValue(KeyPosition.PERCENT_Y, 0);
KeyPosition stick2 = new KeyPosition();
stick2.setFramePosition(mMotionEffectEnd);
stick2.setType(KeyPosition.TYPE_CARTESIAN);
stick2.setValue(KeyPosition.PERCENT_X, 1);
stick2.setValue(KeyPosition.PERCENT_Y, 1);
KeyAttributes translationX1 = null;
KeyAttributes translationX2 = null;
if (mMotionEffectTranslationX > 0) {
translationX1 = new KeyAttributes();
translationX2 = new KeyAttributes();
translationX1.setValue(Key.TRANSLATION_X, mMotionEffectTranslationX);
translationX1.setFramePosition(mMotionEffectEnd);
translationX2.setValue(Key.TRANSLATION_X, 0);
translationX2.setFramePosition(mMotionEffectEnd - 1);
}
KeyAttributes translationY1 = null;
KeyAttributes translationY2 = null;
if (mMotionEffectTranslationY > 0) {
translationY1 = new KeyAttributes();
translationY2 = new KeyAttributes();
translationY1.setValue(Key.TRANSLATION_Y, mMotionEffectTranslationY);
translationY1.setFramePosition(mMotionEffectEnd);
translationY2.setValue(Key.TRANSLATION_Y, 0);
translationY2.setFramePosition(mMotionEffectEnd - 1);
}
int moveDirection = mFadeMove;
if (mFadeMove == AUTO) {
int[] direction = new int[4];
// let's find out the general movement direction for all the referenced views
for (int i = 0; i < views.length; i++) {
MotionController mc = controllerMap.get(views[i]);
if (mc == null) {
continue;
}
float x = mc.getFinalX() - mc.getStartX();
float y = mc.getFinalY() - mc.getStartY();
// look at the direction for this view, and increment the opposite direction
// (as that's the one we will use to apply the fade)
if (y < 0) {
direction[SOUTH]++;
}
if (y > 0) {
direction[NORTH]++;
}
if (x > 0) {
direction[WEST]++;
}
if (x < 0) {
direction[EAST]++;
}
}
int max = direction[0];
moveDirection = 0;
for (int i = 1; i < 4; i++) {
if (max < direction[i]) {
max = direction[i];
moveDirection = i;
}
}
}
for (int i = 0; i < views.length; i++) {
MotionController mc = controllerMap.get(views[i]);
if (mc == null) {
continue;
}
float x = mc.getFinalX() - mc.getStartX();
float y = mc.getFinalY() - mc.getStartY();
boolean apply = true;
// Any view that is moving in the given direction will have the fade applied
// if move strict is true, also include views that are moving in diagonal, even
// if they aren't moving in the opposite direction.
if (moveDirection == NORTH) {
if (y > 0 && (!mMotionEffectStrictMove || x == 0)) {
apply = false;
}
} else if (moveDirection == SOUTH) {
if (y < 0 && (!mMotionEffectStrictMove || x == 0)) {
apply = false;
}
} else if (moveDirection == EAST) {
if (x < 0 && (!mMotionEffectStrictMove || y == 0)) {
apply = false;
}
} else if (moveDirection == WEST) {
if (x > 0 && (!mMotionEffectStrictMove || y == 0)) {
apply = false;
}
}
if (apply) {
if (mViewTransitionId == UNSET) {
mc.addKey(alpha1);
mc.addKey(alpha2);
mc.addKey(stick1);
mc.addKey(stick2);
if (mMotionEffectTranslationX > 0) {
mc.addKey(translationX1);
mc.addKey(translationX2);
}
if (mMotionEffectTranslationY > 0) {
mc.addKey(translationY1);
mc.addKey(translationY2);
}
} else {
motionLayout.applyViewTransition(mViewTransitionId, mc);
}
}
}
}
}