1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.constraintlayout.motion.widget; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.graphics.RectF; 22 import android.util.AttributeSet; 23 import android.util.Log; 24 import android.util.SparseIntArray; 25 import android.util.TypedValue; 26 import android.view.View; 27 import android.view.ViewGroup; 28 29 import androidx.constraintlayout.motion.utils.ViewSpline; 30 import androidx.constraintlayout.widget.ConstraintAttribute; 31 import androidx.constraintlayout.widget.R; 32 33 import java.lang.reflect.Method; 34 import java.util.HashMap; 35 import java.util.HashSet; 36 import java.util.Locale; 37 38 /** 39 * Defines container for a key frame of for storing KeyAttributes. 40 * KeyAttributes change post layout values of a view. 41 * 42 * 43 */ 44 45 public class KeyTrigger extends Key { 46 public static final String VIEW_TRANSITION_ON_CROSS = "viewTransitionOnCross"; 47 public static final String VIEW_TRANSITION_ON_POSITIVE_CROSS = "viewTransitionOnPositiveCross"; 48 public static final String VIEW_TRANSITION_ON_NEGATIVE_CROSS = "viewTransitionOnNegativeCross"; 49 public static final String POST_LAYOUT = "postLayout"; 50 public static final String TRIGGER_SLACK = "triggerSlack"; 51 public static final String TRIGGER_COLLISION_VIEW = "triggerCollisionView"; 52 public static final String TRIGGER_COLLISION_ID = "triggerCollisionId"; 53 public static final String TRIGGER_ID = "triggerID"; 54 public static final String POSITIVE_CROSS = "positiveCross"; 55 public static final String NEGATIVE_CROSS = "negativeCross"; 56 public static final String TRIGGER_RECEIVER = "triggerReceiver"; 57 public static final String CROSS = "CROSS"; 58 public static final int KEY_TYPE = 5; 59 static final String NAME = "KeyTrigger"; 60 private static final String TAG = "KeyTrigger"; 61 float mTriggerSlack = .1f; 62 int mViewTransitionOnNegativeCross = UNSET; 63 int mViewTransitionOnPositiveCross = UNSET; 64 int mViewTransitionOnCross = UNSET; 65 RectF mCollisionRect = new RectF(); 66 RectF mTargetRect = new RectF(); 67 HashMap<String, Method> mMethodHashMap = new HashMap<>(); 68 private int mCurveFit = -1; 69 private String mCross = null; 70 private int mTriggerReceiver = UNSET; 71 private String mNegativeCross = null; 72 private String mPositiveCross = null; 73 private int mTriggerID = UNSET; 74 private int mTriggerCollisionId = UNSET; 75 private View mTriggerCollisionView = null; 76 private boolean mFireCrossReset = true; 77 private boolean mFireNegativeReset = true; 78 private boolean mFirePositiveReset = true; 79 private float mFireThreshold = Float.NaN; 80 private float mFireLastPos; 81 private boolean mPostLayout = false; 82 83 { 84 mType = KEY_TYPE; 85 mCustomConstraints = new HashMap<>(); 86 } 87 88 @Override load(Context context, AttributeSet attrs)89 public void load(Context context, AttributeSet attrs) { 90 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyTrigger); 91 Loader.read(this, a, context); 92 } 93 94 /** 95 * Gets the curve fit type this drives the interpolation 96 * 97 * @return 98 */ getCurveFit()99 int getCurveFit() { 100 return mCurveFit; 101 } 102 103 @Override getAttributeNames(HashSet<String> attributes)104 public void getAttributeNames(HashSet<String> attributes) { 105 } 106 107 @Override addValues(HashMap<String, ViewSpline> splines)108 public void addValues(HashMap<String, ViewSpline> splines) { 109 } 110 111 @Override setValue(String tag, Object value)112 public void setValue(String tag, Object value) { 113 switch (tag) { 114 case CROSS: 115 mCross = value.toString(); 116 break; 117 case TRIGGER_RECEIVER: 118 mTriggerReceiver = toInt(value); 119 break; 120 case NEGATIVE_CROSS: 121 mNegativeCross = value.toString(); 122 break; 123 case POSITIVE_CROSS: 124 mPositiveCross = value.toString(); 125 break; 126 case TRIGGER_ID: 127 mTriggerID = toInt(value); 128 break; 129 case TRIGGER_COLLISION_ID: 130 mTriggerCollisionId = toInt(value); 131 break; 132 case TRIGGER_COLLISION_VIEW: 133 mTriggerCollisionView = (View) value; 134 break; 135 case TRIGGER_SLACK: 136 mTriggerSlack = toFloat(value); 137 break; 138 case POST_LAYOUT: 139 mPostLayout = toBoolean(value); 140 break; 141 case VIEW_TRANSITION_ON_NEGATIVE_CROSS: 142 mViewTransitionOnNegativeCross = toInt(value); 143 break; 144 case VIEW_TRANSITION_ON_POSITIVE_CROSS: 145 mViewTransitionOnPositiveCross = toInt(value); 146 break; 147 case VIEW_TRANSITION_ON_CROSS: 148 mViewTransitionOnCross = toInt(value); 149 break; 150 151 } 152 } 153 setUpRect(RectF rect, View child, boolean postLayout)154 private void setUpRect(RectF rect, View child, boolean postLayout) { 155 rect.top = child.getTop(); 156 rect.bottom = child.getBottom(); 157 rect.left = child.getLeft(); 158 rect.right = child.getRight(); 159 if (postLayout) { 160 child.getMatrix().mapRect(rect); 161 } 162 } 163 164 /** 165 * This fires the keyTriggers associated with this view at that position 166 * 167 * @param pos the progress 168 * @param child the view 169 */ conditionallyFire(float pos, View child)170 public void conditionallyFire(float pos, View child) { 171 boolean fireCross = false; 172 boolean fireNegative = false; 173 boolean firePositive = false; 174 175 if (mTriggerCollisionId != UNSET) { 176 if (mTriggerCollisionView == null) { 177 mTriggerCollisionView = 178 ((ViewGroup) child.getParent()).findViewById(mTriggerCollisionId); 179 } 180 181 setUpRect(mCollisionRect, mTriggerCollisionView, mPostLayout); 182 setUpRect(mTargetRect, child, mPostLayout); 183 boolean in = mCollisionRect.intersect(mTargetRect); 184 // TODO scale by mTriggerSlack 185 if (in) { 186 if (mFireCrossReset) { 187 fireCross = true; 188 mFireCrossReset = false; 189 } 190 if (mFirePositiveReset) { 191 firePositive = true; 192 mFirePositiveReset = false; 193 } 194 mFireNegativeReset = true; 195 } else { 196 if (!mFireCrossReset) { 197 fireCross = true; 198 mFireCrossReset = true; 199 } 200 if (mFireNegativeReset) { 201 fireNegative = true; 202 mFireNegativeReset = false; 203 } 204 mFirePositiveReset = true; 205 } 206 207 } else { 208 209 // Check for crossing 210 if (mFireCrossReset) { 211 212 float offset = pos - mFireThreshold; 213 float lastOffset = mFireLastPos - mFireThreshold; 214 215 if (offset * lastOffset < 0) { // just crossed the threshold 216 fireCross = true; 217 mFireCrossReset = false; 218 } 219 } else { 220 if (Math.abs(pos - mFireThreshold) > mTriggerSlack) { 221 mFireCrossReset = true; 222 } 223 } 224 225 // Check for negative crossing 226 if (mFireNegativeReset) { 227 float offset = pos - mFireThreshold; 228 float lastOffset = mFireLastPos - mFireThreshold; 229 if (offset * lastOffset < 0 && offset < 0) { // just crossed the threshold 230 fireNegative = true; 231 mFireNegativeReset = false; 232 } 233 } else { 234 if (Math.abs(pos - mFireThreshold) > mTriggerSlack) { 235 mFireNegativeReset = true; 236 } 237 } 238 // Check for positive crossing 239 if (mFirePositiveReset) { 240 float offset = pos - mFireThreshold; 241 float lastOffset = mFireLastPos - mFireThreshold; 242 if (offset * lastOffset < 0 && offset > 0) { // just crossed the threshold 243 firePositive = true; 244 mFirePositiveReset = false; 245 } 246 } else { 247 if (Math.abs(pos - mFireThreshold) > mTriggerSlack) { 248 mFirePositiveReset = true; 249 } 250 } 251 } 252 mFireLastPos = pos; 253 254 if (fireNegative || fireCross || firePositive) { 255 ((MotionLayout) child.getParent()).fireTrigger(mTriggerID, firePositive, pos); 256 } 257 View call = (mTriggerReceiver == UNSET) ? child : 258 ((MotionLayout) child.getParent()).findViewById(mTriggerReceiver); 259 260 if (fireNegative) { 261 if (mNegativeCross != null) { 262 fire(mNegativeCross, call); 263 } 264 if (mViewTransitionOnNegativeCross != UNSET) { 265 ((MotionLayout) child.getParent()).viewTransition(mViewTransitionOnNegativeCross, 266 call); 267 } 268 } 269 if (firePositive) { 270 if (mPositiveCross != null) { 271 fire(mPositiveCross, call); 272 } 273 if (mViewTransitionOnPositiveCross != UNSET) { 274 ((MotionLayout) child.getParent()).viewTransition(mViewTransitionOnPositiveCross, 275 call); 276 } 277 } 278 if (fireCross) { 279 if (mCross != null) { 280 fire(mCross, call); 281 } 282 if (mViewTransitionOnCross != UNSET) { 283 ((MotionLayout) child.getParent()).viewTransition(mViewTransitionOnCross, call); 284 } 285 } 286 287 } 288 fire(String str, View call)289 private void fire(String str, View call) { 290 if (str == null) { 291 return; 292 } 293 if (str.startsWith(".")) { 294 fireCustom(str, call); 295 return; 296 } 297 Method method = null; 298 if (mMethodHashMap.containsKey(str)) { 299 method = mMethodHashMap.get(str); 300 if (method == null) { // we looked up and did not find 301 return; 302 } 303 } 304 if (method == null) { 305 try { 306 method = call.getClass().getMethod(str); 307 mMethodHashMap.put(str, method); 308 } catch (NoSuchMethodException e) { 309 mMethodHashMap.put(str, null); // record that we could not get this method 310 Log.e(TAG, "Could not find method \"" + str + "\"" + "on class " 311 + call.getClass().getSimpleName() + " " + Debug.getName(call)); 312 return; 313 } 314 } 315 try { 316 method.invoke(call); 317 } catch (Exception e) { 318 Log.e(TAG, "Exception in call \"" + mCross + "\"" + "on class " 319 + call.getClass().getSimpleName() + " " + Debug.getName(call)); 320 } 321 } 322 fireCustom(String str, View view)323 private void fireCustom(String str, View view) { 324 boolean callAll = str.length() == 1; 325 if (!callAll) { 326 str = str.substring(1).toLowerCase(Locale.ROOT); 327 } 328 for (String name : mCustomConstraints.keySet()) { 329 String lowerCase = name.toLowerCase(Locale.ROOT); 330 if (callAll || lowerCase.matches(str)) { 331 ConstraintAttribute custom = mCustomConstraints.get(name); 332 if (custom != null) { 333 custom.applyCustom(view); 334 } 335 } 336 } 337 } 338 339 /** 340 * Copy the key 341 * 342 * @param src to be copied 343 * @return self 344 */ 345 @Override copy(Key src)346 public Key copy(Key src) { 347 super.copy(src); 348 KeyTrigger k = (KeyTrigger) src; 349 mCurveFit = k.mCurveFit; 350 mCross = k.mCross; 351 mTriggerReceiver = k.mTriggerReceiver; 352 mNegativeCross = k.mNegativeCross; 353 mPositiveCross = k.mPositiveCross; 354 mTriggerID = k.mTriggerID; 355 mTriggerCollisionId = k.mTriggerCollisionId; 356 mTriggerCollisionView = k.mTriggerCollisionView; 357 mTriggerSlack = k.mTriggerSlack; 358 mFireCrossReset = k.mFireCrossReset; 359 mFireNegativeReset = k.mFireNegativeReset; 360 mFirePositiveReset = k.mFirePositiveReset; 361 mFireThreshold = k.mFireThreshold; 362 mFireLastPos = k.mFireLastPos; 363 mPostLayout = k.mPostLayout; 364 mCollisionRect = k.mCollisionRect; 365 mTargetRect = k.mTargetRect; 366 mMethodHashMap = k.mMethodHashMap; 367 return this; 368 } 369 370 /** 371 * Clone this KeyAttributes 372 * 373 * @return 374 */ 375 @Override clone()376 public Key clone() { 377 return new KeyTrigger().copy(this); 378 } 379 380 private static class Loader { 381 private static final int NEGATIVE_CROSS = 1; 382 private static final int POSITIVE_CROSS = 2; 383 private static final int CROSS = 4; 384 private static final int TRIGGER_SLACK = 5; 385 private static final int TRIGGER_ID = 6; 386 private static final int TARGET_ID = 7; 387 private static final int FRAME_POS = 8; 388 private static final int COLLISION = 9; 389 private static final int POST_LAYOUT = 10; 390 private static final int TRIGGER_RECEIVER = 11; 391 private static final int VT_CROSS = 12; 392 private static final int VT_NEGATIVE_CROSS = 13; 393 private static final int VT_POSITIVE_CROSS = 14; 394 395 private static SparseIntArray sAttrMap = new SparseIntArray(); 396 397 static { sAttrMap.append(R.styleable.KeyTrigger_framePosition, FRAME_POS)398 sAttrMap.append(R.styleable.KeyTrigger_framePosition, FRAME_POS); sAttrMap.append(R.styleable.KeyTrigger_onCross, CROSS)399 sAttrMap.append(R.styleable.KeyTrigger_onCross, CROSS); sAttrMap.append(R.styleable.KeyTrigger_onNegativeCross, NEGATIVE_CROSS)400 sAttrMap.append(R.styleable.KeyTrigger_onNegativeCross, NEGATIVE_CROSS); sAttrMap.append(R.styleable.KeyTrigger_onPositiveCross, POSITIVE_CROSS)401 sAttrMap.append(R.styleable.KeyTrigger_onPositiveCross, POSITIVE_CROSS); sAttrMap.append(R.styleable.KeyTrigger_motionTarget, TARGET_ID)402 sAttrMap.append(R.styleable.KeyTrigger_motionTarget, TARGET_ID); sAttrMap.append(R.styleable.KeyTrigger_triggerId, TRIGGER_ID)403 sAttrMap.append(R.styleable.KeyTrigger_triggerId, TRIGGER_ID); sAttrMap.append(R.styleable.KeyTrigger_triggerSlack, TRIGGER_SLACK)404 sAttrMap.append(R.styleable.KeyTrigger_triggerSlack, TRIGGER_SLACK); sAttrMap.append(R.styleable.KeyTrigger_motion_triggerOnCollision, COLLISION)405 sAttrMap.append(R.styleable.KeyTrigger_motion_triggerOnCollision, COLLISION); sAttrMap.append(R.styleable.KeyTrigger_motion_postLayoutCollision, POST_LAYOUT)406 sAttrMap.append(R.styleable.KeyTrigger_motion_postLayoutCollision, POST_LAYOUT); sAttrMap.append(R.styleable.KeyTrigger_triggerReceiver, TRIGGER_RECEIVER)407 sAttrMap.append(R.styleable.KeyTrigger_triggerReceiver, TRIGGER_RECEIVER); sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnCross, VT_CROSS)408 sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnCross, VT_CROSS); sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnNegativeCross, VT_NEGATIVE_CROSS)409 sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnNegativeCross, 410 VT_NEGATIVE_CROSS); sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnPositiveCross, VT_POSITIVE_CROSS)411 sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnPositiveCross, 412 VT_POSITIVE_CROSS); 413 } 414 read(KeyTrigger c, TypedArray a, @SuppressWarnings("unused") Context context)415 public static void read(KeyTrigger c, TypedArray a, 416 @SuppressWarnings("unused") Context context) { 417 final int n = a.getIndexCount(); 418 for (int i = 0; i < n; i++) { 419 int attr = a.getIndex(i); 420 switch (sAttrMap.get(attr)) { 421 case FRAME_POS: 422 c.mFramePosition = a.getInteger(attr, c.mFramePosition); 423 c.mFireThreshold = (c.mFramePosition + .5f) / 100f; 424 break; 425 case TARGET_ID: 426 if (MotionLayout.IS_IN_EDIT_MODE) { 427 c.mTargetId = a.getResourceId(attr, c.mTargetId); 428 if (c.mTargetId == -1) { 429 c.mTargetString = a.getString(attr); 430 } 431 } else { 432 if (a.peekValue(attr).type == TypedValue.TYPE_STRING) { 433 c.mTargetString = a.getString(attr); 434 } else { 435 c.mTargetId = a.getResourceId(attr, c.mTargetId); 436 } 437 } 438 break; 439 case NEGATIVE_CROSS: 440 c.mNegativeCross = a.getString(attr); 441 break; 442 case POSITIVE_CROSS: 443 c.mPositiveCross = a.getString(attr); 444 break; 445 case CROSS: 446 c.mCross = a.getString(attr); 447 break; 448 case TRIGGER_SLACK: 449 c.mTriggerSlack = a.getFloat(attr, c.mTriggerSlack); 450 break; 451 case TRIGGER_ID: 452 c.mTriggerID = a.getResourceId(attr, c.mTriggerID); 453 break; 454 case COLLISION: 455 c.mTriggerCollisionId = a.getResourceId(attr, c.mTriggerCollisionId); 456 break; 457 case POST_LAYOUT: 458 c.mPostLayout = a.getBoolean(attr, c.mPostLayout); 459 break; 460 case TRIGGER_RECEIVER: 461 c.mTriggerReceiver = a.getResourceId(attr, c.mTriggerReceiver); 462 break; 463 case VT_NEGATIVE_CROSS: 464 c.mViewTransitionOnNegativeCross = a.getResourceId(attr, 465 c.mViewTransitionOnNegativeCross); 466 break; 467 case VT_POSITIVE_CROSS: 468 c.mViewTransitionOnPositiveCross = a.getResourceId(attr, 469 c.mViewTransitionOnPositiveCross); 470 break; 471 case VT_CROSS: 472 c.mViewTransitionOnCross = a.getResourceId(attr, c.mViewTransitionOnCross); 473 break; 474 default: 475 Log.e(NAME, "unused attribute 0x" + Integer.toHexString(attr) 476 + " " + sAttrMap.get(attr)); 477 break; 478 } 479 } 480 } 481 } 482 } 483