Advertisement
Guest User

Untitled

a guest
Feb 21st, 2020
95
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 55.58 KB | None | 0 0
  1. //TapTarget
  2.  
  3. }static class UiUtil {
  4. UiUtil() {
  5. }
  6. static int dp(Context context, int val) {
  7. return (int) TypedValue.applyDimension(
  8. TypedValue.COMPLEX_UNIT_DIP, val, context.getResources().getDisplayMetrics());
  9. }
  10. static int sp(Context context, int val) {
  11. return (int) TypedValue.applyDimension(
  12. TypedValue.COMPLEX_UNIT_SP, val, context.getResources().getDisplayMetrics());
  13. }
  14. static int themeIntAttr(Context context, String attr) {
  15. final android.content.res.Resources.Theme theme = context.getTheme();
  16. if (theme == null) {
  17. return -1;
  18. }
  19. final TypedValue value = new TypedValue();
  20. final int id = context.getResources().getIdentifier(attr, "attr", context.getPackageName());
  21.  
  22. if (id == 0) {
  23. // Not found
  24. return -1;
  25. }
  26. theme.resolveAttribute(id, value, true);
  27. return value.data;
  28. }
  29. static int setAlpha(int argb, float alpha) {
  30. if (alpha > 1.0f) {
  31. alpha = 1.0f;
  32. } else if (alpha <= 0.0f) {
  33. alpha = 0.0f;
  34. }
  35. return ((int) ((argb >>> 24) * alpha) << 24) | (argb & 0x00FFFFFF);
  36. }
  37. }
  38. static class FloatValueAnimatorBuilder {
  39.  
  40. private final ValueAnimator animator;
  41.  
  42. private EndListener endListener;
  43.  
  44. interface UpdateListener {
  45. void onUpdate(float lerpTime);
  46. }
  47. interface EndListener {
  48. void onEnd();
  49. }
  50. protected FloatValueAnimatorBuilder() {
  51. this(false);
  52. }
  53. FloatValueAnimatorBuilder(boolean reverse) {
  54. if (reverse) {
  55. this.animator = ValueAnimator.ofFloat(1.0f, 0.0f);
  56. } else {
  57. this.animator = ValueAnimator.ofFloat(0.0f, 1.0f);
  58. }
  59. }
  60. public FloatValueAnimatorBuilder delayBy(long millis) {
  61. animator.setStartDelay(millis);
  62. return this;
  63. }
  64. public FloatValueAnimatorBuilder duration(long millis) {
  65. animator.setDuration(millis);
  66. return this;
  67. }
  68. public FloatValueAnimatorBuilder interpolator(TimeInterpolator lerper) {
  69. animator.setInterpolator(lerper);
  70. return this;
  71. }
  72. public FloatValueAnimatorBuilder repeat(int times) {
  73. animator.setRepeatCount(times);
  74. return this;
  75. }
  76. public FloatValueAnimatorBuilder onUpdate(final UpdateListener listener) {
  77. animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  78. @Override
  79. public void onAnimationUpdate(ValueAnimator animation) {
  80. listener.onUpdate((float) animation.getAnimatedValue());
  81. }
  82. });
  83. return this;
  84. }
  85. public FloatValueAnimatorBuilder onEnd(final EndListener listener) {
  86. this.endListener = listener;
  87. return this;
  88. }
  89. public ValueAnimator build() {
  90. if (endListener != null) {
  91. animator.addListener(new AnimatorListenerAdapter() {
  92. @Override
  93. public void onAnimationEnd(Animator animation) {
  94. endListener.onEnd();
  95. }
  96. });
  97. }
  98. return animator;
  99. }
  100. }
  101. static class ReflectUtil {
  102. ReflectUtil() {
  103. }
  104. static Object getPrivateField(Object source, String fieldName)
  105. throws NoSuchFieldException, IllegalAccessException {
  106. final java.lang.reflect.Field objectField = source.getClass().getDeclaredField(fieldName);
  107. objectField.setAccessible(true);
  108. return objectField.get(source);
  109. }
  110. }
  111. static class TapTarget extends Activity {
  112. final CharSequence title;
  113. final CharSequence description;
  114. float outerCircleAlpha = 0.96f;
  115. int targetRadius = 44;
  116. Rect bounds;
  117. android.graphics.drawable.Drawable icon;
  118. Typeface titleTypeface;
  119. Typeface descriptionTypeface;
  120.  
  121.  
  122. private int outerCircleColorRes = -1;
  123. private int targetCircleColorRes = -1;
  124. private int dimColorRes = -1;
  125. private int titleTextColorRes = -1;
  126. private int descriptionTextColorRes = -1;
  127.  
  128. private Integer outerCircleColor = null;
  129. private Integer targetCircleColor = null;
  130. private Integer dimColor = null;
  131. private Integer titleTextColor = null;
  132. private Integer descriptionTextColor = null;
  133.  
  134. private int titleTextDimen = -1;
  135. private int descriptionTextDimen = -1;
  136. private int titleTextSize = 20;
  137. private int descriptionTextSize = 18;
  138. int id = -1;
  139. boolean drawShadow = false;
  140. boolean cancelable = true;
  141. boolean tintTarget = true;
  142. boolean transparentTarget = false;
  143. float descriptionTextAlpha = 0.54f;
  144.  
  145. public static TapTarget forView(View view, CharSequence title) {
  146. return forView(view, title, null);
  147. }
  148. public static TapTarget forView(View view, CharSequence title, CharSequence description) {
  149. return new ViewTapTarget(view, title, description);
  150. }
  151. public static TapTarget forBounds(Rect bounds, CharSequence title) {
  152. return forBounds(bounds, title, null);
  153. }
  154. public static TapTarget forBounds(Rect bounds, CharSequence title, CharSequence description) {
  155. return new TapTarget(bounds, title, description);
  156. }
  157. protected TapTarget(Rect bounds, CharSequence title, CharSequence description) {
  158. this(title, description);
  159. if (bounds == null) {
  160. throw new IllegalArgumentException("Cannot pass null bounds or title");
  161. }
  162. this.bounds = bounds;
  163. }
  164. protected TapTarget(CharSequence title, CharSequence description) {
  165. if (title == null) {
  166. throw new IllegalArgumentException("Cannot pass null title");
  167. }
  168. this.title = title;
  169. this.description = description;
  170. }
  171. public TapTarget transparentTarget(boolean transparent) {
  172. this.transparentTarget = transparent;
  173. return this;
  174. }
  175. public TapTarget outerCircleColor( int color) {
  176. this.outerCircleColorRes = color;
  177. return this;
  178. }
  179. public TapTarget outerCircleColorInt( int color) {
  180. this.outerCircleColor = color;
  181. return this;
  182. }
  183. public TapTarget outerCircleAlpha(float alpha) {
  184. if (alpha < 0.0f || alpha > 1.0f) {
  185. throw new IllegalArgumentException("Given an invalid alpha value: " + alpha);
  186. }
  187. this.outerCircleAlpha = alpha;
  188. return this;
  189. }
  190. public TapTarget targetCircleColor( int color) {
  191. this.targetCircleColorRes = color;
  192. return this;
  193. }
  194. public TapTarget targetCircleColorInt( int color) {
  195. this.targetCircleColor = color;
  196. return this;
  197. }
  198. public TapTarget textColor( int color) {
  199. this.titleTextColorRes = color;
  200. this.descriptionTextColorRes = color;
  201. return this;
  202. }
  203. public TapTarget textColorInt( int color) {
  204. this.titleTextColor = color;
  205. this.descriptionTextColor = color;
  206. return this;
  207. }
  208. public TapTarget titleTextColor( int color) {
  209. this.titleTextColorRes = color;
  210. return this;
  211. }
  212. public TapTarget titleTextColorInt( int color) {
  213. this.titleTextColor = color;
  214. return this;
  215. }
  216. public TapTarget descriptionTextColor( int color) {
  217. this.descriptionTextColorRes = color;
  218. return this;
  219. }
  220. public TapTarget descriptionTextColorInt( int color) {
  221. this.descriptionTextColor = color;
  222. return this;
  223. }
  224. public TapTarget textTypeface(Typeface typeface) {
  225. if (typeface == null) throw new IllegalArgumentException("Cannot use a null typeface");
  226. titleTypeface = typeface;
  227. descriptionTypeface = typeface;
  228. return this;
  229. }
  230. public TapTarget titleTypeface(Typeface titleTypeface) {
  231. if (titleTypeface == null) throw new IllegalArgumentException("Cannot use a null typeface");
  232. this.titleTypeface = titleTypeface;
  233. return this;
  234. }
  235. public TapTarget descriptionTypeface(Typeface descriptionTypeface) {
  236. if (descriptionTypeface == null) throw new IllegalArgumentException("Cannot use a null typeface");
  237. this.descriptionTypeface = descriptionTypeface;
  238. return this;
  239. }
  240. public TapTarget titleTextSize(int sp) {
  241. if (sp < 0) throw new IllegalArgumentException("Given negative text size");
  242. this.titleTextSize = sp;
  243. return this;
  244. }
  245. public TapTarget descriptionTextSize(int sp) {
  246. if (sp < 0) throw new IllegalArgumentException("Given negative text size");
  247. this.descriptionTextSize = sp;
  248. return this;
  249. }
  250. public TapTarget titleTextDimen( int dimen) {
  251. this.titleTextDimen = dimen;
  252. return this;
  253. }
  254. public TapTarget descriptionTextAlpha(float descriptionTextAlpha) {
  255. if (descriptionTextAlpha < 0 || descriptionTextAlpha > 1f) {
  256. throw new IllegalArgumentException("Given an invalid alpha value: " + descriptionTextAlpha);
  257. }
  258. this.descriptionTextAlpha = descriptionTextAlpha;
  259. return this;
  260. }
  261. public TapTarget descriptionTextDimen( int dimen) {
  262. this.descriptionTextDimen = dimen;
  263. return this;
  264. }
  265. public TapTarget dimColor( int color) {
  266. this.dimColorRes = color;
  267. return this;
  268. }
  269. public TapTarget dimColorInt( int color) {
  270. this.dimColor = color;
  271. return this;
  272. }
  273. public TapTarget drawShadow(boolean draw) {
  274. this.drawShadow = draw;
  275. return this;
  276. }
  277. public TapTarget cancelable(boolean status) {
  278. this.cancelable = status;
  279. return this;
  280. }
  281. public TapTarget tintTarget(boolean tint) {
  282. this.tintTarget = tint;
  283. return this;
  284. }
  285. public TapTarget icon(android.graphics.drawable.Drawable icon) {
  286. return icon(icon, false);
  287. }
  288. public TapTarget icon(android.graphics.drawable.Drawable icon, boolean hasSetBounds) {
  289. if (icon == null) throw new IllegalArgumentException("Cannot use null drawable");
  290. this.icon = icon;
  291. if (!hasSetBounds) {
  292. this.icon.setBounds(new Rect(0, 0, this.icon.getIntrinsicWidth(), this.icon.getIntrinsicHeight()));
  293. }
  294. return this;
  295. }
  296. public TapTarget id(int id) {
  297. this.id = id;
  298. return this;
  299. }
  300. public TapTarget targetRadius(int targetRadius) {
  301. this.targetRadius = targetRadius;
  302. return this;
  303. }
  304. public int id() {
  305. return id;
  306. }
  307. public void onReady(Runnable runnable) {
  308. runnable.run();
  309. }
  310. public Rect bounds() {
  311. if (bounds == null) {
  312. throw new IllegalStateException("Requesting bounds that are not set! Make sure your target is ready");
  313. }
  314. return bounds;
  315. }
  316. Integer outerCircleColorInt(Context context) {
  317. return colorResOrInt(context, outerCircleColor, outerCircleColorRes);
  318. }
  319. Integer targetCircleColorInt(Context context) {
  320. return colorResOrInt(context, targetCircleColor, targetCircleColorRes);
  321. }
  322. Integer dimColorInt(Context context) {
  323. return colorResOrInt(context, dimColor, dimColorRes);
  324. }
  325. Integer titleTextColorInt(Context context) {
  326. return colorResOrInt(context, titleTextColor, titleTextColorRes);
  327. }
  328.  
  329. Integer descriptionTextColorInt(Context context) {
  330. return colorResOrInt(context, descriptionTextColor, descriptionTextColorRes);
  331. }
  332. int titleTextSizePx(Context context) {
  333. return dimenOrSize(context, titleTextSize, titleTextDimen);
  334. }
  335. int descriptionTextSizePx(Context context) {
  336. return dimenOrSize(context, descriptionTextSize, descriptionTextDimen);
  337. }
  338.  
  339. private Integer colorResOrInt(Context context, Integer value, int resource) {
  340. if (resource != -1) {
  341. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  342. return context.getColor(resource);
  343. }
  344. }
  345. return value;
  346. }
  347. private int dimenOrSize(Context context, int size, int dimen) {
  348. if (dimen != -1) {
  349. return context.getResources().getDimensionPixelSize(dimen);
  350. }
  351. return UiUtil.sp(context, size);
  352. }
  353. }
  354. static class TapTargetView extends View {
  355. private boolean isDismissed = false;
  356. private boolean isDismissing = false;
  357. private boolean isInteractable = true;
  358.  
  359. final int TARGET_PADDING;
  360. final int TARGET_RADIUS;
  361. final int TARGET_PULSE_RADIUS;
  362. final int TEXT_PADDING;
  363. final int TEXT_SPACING;
  364. final int TEXT_MAX_WIDTH;
  365. final int TEXT_POSITIONING_BIAS;
  366. final int CIRCLE_PADDING;
  367. final int GUTTER_DIM;
  368. final int SHADOW_DIM;
  369. final int SHADOW_JITTER_DIM;
  370.  
  371.  
  372. final ViewGroup boundingParent;
  373. final ViewManager parent;
  374. final TapTarget target;
  375. final Rect targetBounds;
  376.  
  377. final TextPaint titlePaint;
  378. final TextPaint descriptionPaint;
  379. final Paint outerCirclePaint;
  380. final Paint outerCircleShadowPaint;
  381. final Paint targetCirclePaint;
  382. final Paint targetCirclePulsePaint;
  383.  
  384. CharSequence title;
  385.  
  386. StaticLayout titleLayout;
  387.  
  388. CharSequence description;
  389.  
  390. StaticLayout descriptionLayout;
  391. boolean isDark;
  392. boolean debug;
  393. boolean shouldTintTarget;
  394. boolean shouldDrawShadow;
  395. boolean cancelable;
  396. boolean visible;
  397.  
  398. // Debug related variables
  399.  
  400. SpannableStringBuilder debugStringBuilder;
  401.  
  402. DynamicLayout debugLayout;
  403.  
  404. TextPaint debugTextPaint;
  405.  
  406. Paint debugPaint;
  407.  
  408. // Drawing properties
  409. Rect drawingBounds;
  410. Rect textBounds;
  411.  
  412. Path outerCirclePath;
  413. float outerCircleRadius;
  414. int calculatedOuterCircleRadius;
  415. int[] outerCircleCenter;
  416. int outerCircleAlpha;
  417.  
  418. float targetCirclePulseRadius;
  419. int targetCirclePulseAlpha;
  420.  
  421. float targetCircleRadius;
  422. int targetCircleAlpha;
  423.  
  424. int textAlpha;
  425. int dimColor;
  426.  
  427. float lastTouchX;
  428. float lastTouchY;
  429.  
  430. int topBoundary;
  431. int bottomBoundary;
  432.  
  433. Bitmap tintedTarget;
  434.  
  435. Listener listener;
  436.  
  437.  
  438. ViewOutlineProvider outlineProvider;
  439.  
  440. public static TapTargetView showFor(Activity activity, TapTarget target) {
  441. return showFor(activity, target, null);
  442. }
  443.  
  444. public static TapTargetView showFor(Activity activity, TapTarget target, Listener listener) {
  445. if (activity == null) throw new IllegalArgumentException("Activity is null");
  446.  
  447. final ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
  448. final ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
  449. ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
  450. final ViewGroup content = (ViewGroup) decor.findViewById(android.R.id.content);
  451. final TapTargetView tapTargetView = new TapTargetView(activity, decor, content, target, listener);
  452. decor.addView(tapTargetView, layoutParams);
  453.  
  454. return tapTargetView;
  455. }
  456.  
  457. public static TapTargetView showFor(Dialog dialog, TapTarget target) {
  458. return showFor(dialog, target, null);
  459. }
  460.  
  461. public static TapTargetView showFor(Dialog dialog, TapTarget target, Listener listener) {
  462. if (dialog == null) throw new IllegalArgumentException("Dialog is null");
  463.  
  464. final Context context = dialog.getContext();
  465. final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
  466. final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
  467. params.type = WindowManager.LayoutParams.TYPE_APPLICATION;
  468. params.format = PixelFormat.RGBA_8888;
  469. params.flags = 0;
  470. params.gravity = Gravity.START | Gravity.TOP;
  471. params.x = 0;
  472. params.y = 0;
  473. params.width = WindowManager.LayoutParams.MATCH_PARENT;
  474. params.height = WindowManager.LayoutParams.MATCH_PARENT;
  475.  
  476. final TapTargetView tapTargetView = new TapTargetView(context, windowManager, null, target, listener);
  477. windowManager.addView(tapTargetView, params);
  478.  
  479. return tapTargetView;
  480. }
  481.  
  482. public static class Listener {
  483. /** Signals that the user has clicked inside of the target **/
  484. public void onTargetClick(TapTargetView view) {
  485. view.dismiss(true);
  486. }
  487.  
  488. /** Signals that the user has long clicked inside of the target **/
  489. public void onTargetLongClick(TapTargetView view) {
  490. onTargetClick(view);
  491. }
  492.  
  493. /** If cancelable, signals that the user has clicked outside of the outer circle **/
  494. public void onTargetCancel(TapTargetView view) {
  495. view.dismiss(false);
  496. }
  497.  
  498. /** Signals that the user clicked on the outer circle portion of the tap target **/
  499. public void onOuterCircleClick(TapTargetView view) {
  500. // no-op as default
  501. }
  502.  
  503. /**
  504. * Signals that the tap target has been dismissed
  505. * @param userInitiated Whether the user caused this action
  506. *
  507. *
  508. */
  509. public void onTargetDismissed(TapTargetView view, boolean userInitiated) {
  510. }
  511. }
  512.  
  513. final FloatValueAnimatorBuilder.UpdateListener expandContractUpdateListener = new FloatValueAnimatorBuilder.UpdateListener() {
  514. @Override
  515. public void onUpdate(float lerpTime) {
  516. final float newOuterCircleRadius = calculatedOuterCircleRadius * lerpTime;
  517. final boolean expanding = newOuterCircleRadius > outerCircleRadius;
  518. if (!expanding) {
  519. // When contracting we need to invalidate the old drawing bounds. Otherwise
  520. // you will see artifacts as the circle gets smaller
  521. calculateDrawingBounds();
  522. }
  523.  
  524. final float targetAlpha = target.outerCircleAlpha * 255;
  525. outerCircleRadius = newOuterCircleRadius;
  526. outerCircleAlpha = (int) Math.min(targetAlpha, (lerpTime * 1.5f * targetAlpha));
  527. outerCirclePath.reset();
  528. outerCirclePath.addCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, Path.Direction.CW);
  529.  
  530. targetCircleAlpha = (int) Math.min(255.0f, (lerpTime * 1.5f * 255.0f));
  531.  
  532. if (expanding) {
  533. targetCircleRadius = TARGET_RADIUS * Math.min(1.0f, lerpTime * 1.5f);
  534. } else {
  535. targetCircleRadius = TARGET_RADIUS * lerpTime;
  536. targetCirclePulseRadius *= lerpTime;
  537. }
  538.  
  539. textAlpha = (int) (delayedLerp(lerpTime, 0.7f) * 255);
  540.  
  541. if (expanding) {
  542. calculateDrawingBounds();
  543. }
  544.  
  545. invalidateViewAndOutline(drawingBounds);
  546. }
  547. };
  548.  
  549. final ValueAnimator expandAnimation = new FloatValueAnimatorBuilder()
  550. .duration(250)
  551. .delayBy(250)
  552. .interpolator(new AccelerateDecelerateInterpolator())
  553. .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
  554. @Override
  555. public void onUpdate(float lerpTime) {
  556. expandContractUpdateListener.onUpdate(lerpTime);
  557. }
  558. })
  559. .onEnd(new FloatValueAnimatorBuilder.EndListener() {
  560. @Override
  561. public void onEnd() {
  562. pulseAnimation.start();
  563. isInteractable = true;
  564. }
  565. })
  566. .build();
  567.  
  568. final ValueAnimator pulseAnimation = new FloatValueAnimatorBuilder()
  569. .duration(1000)
  570. .repeat(ValueAnimator.INFINITE)
  571. .interpolator(new AccelerateDecelerateInterpolator())
  572. .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
  573. @Override
  574. public void onUpdate(float lerpTime) {
  575. final float pulseLerp = delayedLerp(lerpTime, 0.5f);
  576. targetCirclePulseRadius = (1.0f + pulseLerp) * TARGET_RADIUS;
  577. targetCirclePulseAlpha = (int) ((1.0f - pulseLerp) * 255);
  578. targetCircleRadius = TARGET_RADIUS + halfwayLerp(lerpTime) * TARGET_PULSE_RADIUS;
  579.  
  580. if (outerCircleRadius != calculatedOuterCircleRadius) {
  581. outerCircleRadius = calculatedOuterCircleRadius;
  582. }
  583.  
  584. calculateDrawingBounds();
  585. invalidateViewAndOutline(drawingBounds);
  586. }
  587. })
  588. .build();
  589.  
  590. final ValueAnimator dismissAnimation = new FloatValueAnimatorBuilder(true)
  591. .duration(250)
  592. .interpolator(new AccelerateDecelerateInterpolator())
  593. .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
  594. @Override
  595. public void onUpdate(float lerpTime) {
  596. expandContractUpdateListener.onUpdate(lerpTime);
  597. }
  598. })
  599. .onEnd(new FloatValueAnimatorBuilder.EndListener() {
  600. @Override
  601. public void onEnd() {
  602. onDismiss(true);
  603. ViewUtil.removeView(parent, TapTargetView.this);
  604. }
  605. })
  606. .build();
  607.  
  608. private final ValueAnimator dismissConfirmAnimation = new FloatValueAnimatorBuilder()
  609. .duration(250)
  610. .interpolator(new AccelerateDecelerateInterpolator())
  611. .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
  612. @Override
  613. public void onUpdate(float lerpTime) {
  614. final float spedUpLerp = Math.min(1.0f, lerpTime * 2.0f);
  615. outerCircleRadius = calculatedOuterCircleRadius * (1.0f + (spedUpLerp * 0.2f));
  616. outerCircleAlpha = (int) ((1.0f - spedUpLerp) * target.outerCircleAlpha * 255.0f);
  617. outerCirclePath.reset();
  618. outerCirclePath.addCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, Path.Direction.CW);
  619. targetCircleRadius = (1.0f - lerpTime) * TARGET_RADIUS;
  620. targetCircleAlpha = (int) ((1.0f - lerpTime) * 255.0f);
  621. targetCirclePulseRadius = (1.0f + lerpTime) * TARGET_RADIUS;
  622. targetCirclePulseAlpha = (int) ((1.0f - lerpTime) * targetCirclePulseAlpha);
  623. textAlpha = (int) ((1.0f - spedUpLerp) * 255.0f);
  624. calculateDrawingBounds();
  625. invalidateViewAndOutline(drawingBounds);
  626. }
  627. })
  628. .onEnd(new FloatValueAnimatorBuilder.EndListener() {
  629. @Override
  630. public void onEnd() {
  631. onDismiss(true);
  632. ViewUtil.removeView(parent, TapTargetView.this);
  633. }
  634. })
  635. .build();
  636.  
  637. private ValueAnimator[] animators = new ValueAnimator[]
  638. {expandAnimation, pulseAnimation, dismissConfirmAnimation, dismissAnimation};
  639.  
  640. private final ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener;
  641. public TapTargetView(final Context context,
  642. final ViewManager parent,
  643. final ViewGroup boundingParent,
  644. final TapTarget target,
  645. final Listener userListener) {
  646. super(context);
  647. if (target == null) throw new IllegalArgumentException("Target cannot be null");
  648.  
  649. this.target = target;
  650. this.parent = parent;
  651. this.boundingParent = boundingParent;
  652. this.listener = userListener != null ? userListener : new Listener();
  653. this.title = target.title;
  654. this.description = target.description;
  655.  
  656. TARGET_PADDING = UiUtil.dp(context, 20);
  657. CIRCLE_PADDING = UiUtil.dp(context, 40);
  658. TARGET_RADIUS = UiUtil.dp(context, target.targetRadius);
  659. TEXT_PADDING = UiUtil.dp(context, 40);
  660. TEXT_SPACING = UiUtil.dp(context, 8);
  661. TEXT_MAX_WIDTH = UiUtil.dp(context, 360);
  662. TEXT_POSITIONING_BIAS = UiUtil.dp(context, 20);
  663. GUTTER_DIM = UiUtil.dp(context, 88);
  664. SHADOW_DIM = UiUtil.dp(context, 8);
  665. SHADOW_JITTER_DIM = UiUtil.dp(context, 1);
  666. TARGET_PULSE_RADIUS = (int) (0.1f * TARGET_RADIUS);
  667.  
  668. outerCirclePath = new Path();
  669. targetBounds = new Rect();
  670. drawingBounds = new Rect();
  671.  
  672. titlePaint = new TextPaint();
  673. titlePaint.setTextSize(target.titleTextSizePx(context));
  674. titlePaint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
  675. titlePaint.setAntiAlias(true);
  676.  
  677. descriptionPaint = new TextPaint();
  678. descriptionPaint.setTextSize(target.descriptionTextSizePx(context));
  679. descriptionPaint.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL));
  680. descriptionPaint.setAntiAlias(true);
  681. descriptionPaint.setAlpha((int) (0.54f * 255.0f));
  682.  
  683. outerCirclePaint = new Paint();
  684. outerCirclePaint.setAntiAlias(true);
  685. outerCirclePaint.setAlpha((int) (target.outerCircleAlpha * 255.0f));
  686.  
  687. outerCircleShadowPaint = new Paint();
  688. outerCircleShadowPaint.setAntiAlias(true);
  689. outerCircleShadowPaint.setAlpha(50);
  690. outerCircleShadowPaint.setStyle(Paint.Style.STROKE);
  691. outerCircleShadowPaint.setStrokeWidth(SHADOW_JITTER_DIM);
  692. outerCircleShadowPaint.setColor(Color.BLACK);
  693.  
  694. targetCirclePaint = new Paint();
  695. targetCirclePaint.setAntiAlias(true);
  696.  
  697. targetCirclePulsePaint = new Paint();
  698. targetCirclePulsePaint.setAntiAlias(true);
  699.  
  700. applyTargetOptions(context);
  701.  
  702. globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
  703. @Override
  704. public void onGlobalLayout() {
  705. if (isDismissing) {
  706. return;
  707. }
  708. updateTextLayouts();
  709. target.onReady(new Runnable() {
  710. @Override
  711. public void run() {
  712. final int[] offset = new int[2];
  713.  
  714. targetBounds.set(target.bounds());
  715.  
  716. getLocationOnScreen(offset);
  717. targetBounds.offset(-offset[0], -offset[1]);
  718.  
  719. if (boundingParent != null) {
  720. final WindowManager windowManager
  721. = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
  722. final DisplayMetrics displayMetrics = new DisplayMetrics();
  723. windowManager.getDefaultDisplay().getMetrics(displayMetrics);
  724.  
  725. final Rect rect = new Rect();
  726. boundingParent.getWindowVisibleDisplayFrame(rect);
  727.  
  728. // We bound the boundaries to be within the screen's coordinates to
  729. // handle the case where the layout bounds do not match
  730. // (like when FLAG_LAYOUT_NO_LIMITS is specified)
  731. topBoundary = Math.max(0, rect.top);
  732. bottomBoundary = Math.min(rect.bottom, displayMetrics.heightPixels);
  733. }
  734.  
  735. drawTintedTarget();
  736. requestFocus();
  737. calculateDimensions();
  738.  
  739. startExpandAnimation();
  740. }
  741. });
  742. }
  743. };
  744.  
  745. getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
  746.  
  747. setFocusableInTouchMode(true);
  748. setClickable(true);
  749. setOnClickListener(new OnClickListener() {
  750. @Override
  751. public void onClick(View v) {
  752. if (listener == null || outerCircleCenter == null || !isInteractable) return;
  753.  
  754. final boolean clickedInTarget =
  755. distance(targetBounds.centerX(), targetBounds.centerY(), (int) lastTouchX, (int) lastTouchY) <= targetCircleRadius;
  756. final double distanceToOuterCircleCenter = distance(outerCircleCenter[0], outerCircleCenter[1],
  757. (int) lastTouchX, (int) lastTouchY);
  758. final boolean clickedInsideOfOuterCircle = distanceToOuterCircleCenter <= outerCircleRadius;
  759.  
  760. if (clickedInTarget) {
  761. isInteractable = false;
  762. listener.onTargetClick(TapTargetView.this);
  763. } else if (clickedInsideOfOuterCircle) {
  764. listener.onOuterCircleClick(TapTargetView.this);
  765. } else if (cancelable) {
  766. isInteractable = false;
  767. listener.onTargetCancel(TapTargetView.this);
  768. }
  769. }
  770. });
  771.  
  772. setOnLongClickListener(new OnLongClickListener() {
  773. @Override
  774. public boolean onLongClick(View v) {
  775. if (listener == null) return false;
  776.  
  777. if (targetBounds.contains((int) lastTouchX, (int) lastTouchY)) {
  778. listener.onTargetLongClick(TapTargetView.this);
  779. return true;
  780. }
  781.  
  782. return false;
  783. }
  784. });
  785. }
  786.  
  787. private void startExpandAnimation() {
  788. if (!visible) {
  789. isInteractable = false;
  790. expandAnimation.start();
  791. visible = true;
  792. }
  793. }
  794.  
  795. protected void applyTargetOptions(Context context) {
  796. shouldTintTarget = target.tintTarget;
  797. shouldDrawShadow = target.drawShadow;
  798. cancelable = target.cancelable;
  799.  
  800. // We can't clip out portions of a view outline, so if the user specified a transparent
  801. // target, we need to fallback to drawing a jittered shadow approximation
  802. if (shouldDrawShadow && Build.VERSION.SDK_INT >= 21 && !target.transparentTarget) {
  803. outlineProvider = new ViewOutlineProvider() {
  804. @Override
  805. public void getOutline(View view, Outline outline) {
  806. if (outerCircleCenter == null) return;
  807. outline.setOval(
  808. (int) (outerCircleCenter[0] - outerCircleRadius), (int) (outerCircleCenter[1] - outerCircleRadius),
  809. (int) (outerCircleCenter[0] + outerCircleRadius), (int) (outerCircleCenter[1] + outerCircleRadius));
  810. outline.setAlpha(outerCircleAlpha / 255.0f);
  811. if (Build.VERSION.SDK_INT >= 22) {
  812. outline.offset(0, SHADOW_DIM);
  813. }
  814. }
  815. };
  816.  
  817. setOutlineProvider(outlineProvider);
  818. setElevation(SHADOW_DIM);
  819. }
  820.  
  821. if (shouldDrawShadow && outlineProvider == null && Build.VERSION.SDK_INT < 18) {
  822. setLayerType(LAYER_TYPE_SOFTWARE, null);
  823. } else {
  824. setLayerType(LAYER_TYPE_HARDWARE, null);
  825. }
  826.  
  827. final android.content.res.Resources.Theme theme = context.getTheme();
  828. isDark = UiUtil.themeIntAttr(context, "isLightTheme") == 0;
  829.  
  830. final Integer outerCircleColor = target.outerCircleColorInt(context);
  831. if (outerCircleColor != null) {
  832. outerCirclePaint.setColor(outerCircleColor);
  833. } else if (theme != null) {
  834. outerCirclePaint.setColor(UiUtil.themeIntAttr(context, "colorPrimary"));
  835. } else {
  836. outerCirclePaint.setColor(Color.WHITE);
  837. }
  838.  
  839. final Integer targetCircleColor = target.targetCircleColorInt(context);
  840. if (targetCircleColor != null) {
  841. targetCirclePaint.setColor(targetCircleColor);
  842. } else {
  843. targetCirclePaint.setColor(isDark ? Color.BLACK : Color.WHITE);
  844. }
  845.  
  846. if (target.transparentTarget) {
  847. targetCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
  848. }
  849.  
  850. targetCirclePulsePaint.setColor(targetCirclePaint.getColor());
  851.  
  852. final Integer targetDimColor = target.dimColorInt(context);
  853. if (targetDimColor != null) {
  854. dimColor = UiUtil.setAlpha(targetDimColor, 0.3f);
  855. } else {
  856. dimColor = -1;
  857. }
  858.  
  859. final Integer titleTextColor = target.titleTextColorInt(context);
  860. if (titleTextColor != null) {
  861. titlePaint.setColor(titleTextColor);
  862. } else {
  863. titlePaint.setColor(isDark ? Color.BLACK : Color.WHITE);
  864. }
  865.  
  866. final Integer descriptionTextColor = target.descriptionTextColorInt(context);
  867. if (descriptionTextColor != null) {
  868. descriptionPaint.setColor(descriptionTextColor);
  869. } else {
  870. descriptionPaint.setColor(titlePaint.getColor());
  871. }
  872.  
  873. if (target.titleTypeface != null) {
  874. titlePaint.setTypeface(target.titleTypeface);
  875. }
  876.  
  877. if (target.descriptionTypeface != null) {
  878. descriptionPaint.setTypeface(target.descriptionTypeface);
  879. }
  880. }
  881.  
  882. @Override
  883. protected void onDetachedFromWindow() {
  884. super.onDetachedFromWindow();
  885. onDismiss(false);
  886. }
  887.  
  888. void onDismiss(boolean userInitiated) {
  889. if (isDismissed) return;
  890.  
  891. isDismissing = false;
  892. isDismissed = true;
  893.  
  894. for (final ValueAnimator animator : animators) {
  895. animator.cancel();
  896. animator.removeAllUpdateListeners();
  897. }
  898. ViewUtil.removeOnGlobalLayoutListener(getViewTreeObserver(), globalLayoutListener);
  899. visible = false;
  900.  
  901. if (listener != null) {
  902. listener.onTargetDismissed(this, userInitiated);
  903. }
  904. }
  905.  
  906. @Override
  907. protected void onDraw(Canvas c) {
  908. if (isDismissed || outerCircleCenter == null) return;
  909.  
  910. if (topBoundary > 0 && bottomBoundary > 0) {
  911. c.clipRect(0, topBoundary, getWidth(), bottomBoundary);
  912. }
  913.  
  914. if (dimColor != -1) {
  915. c.drawColor(dimColor);
  916. }
  917.  
  918. int saveCount;
  919. outerCirclePaint.setAlpha(outerCircleAlpha);
  920. if (shouldDrawShadow && outlineProvider == null) {
  921. saveCount = c.save();
  922. {
  923. c.clipPath(outerCirclePath, Region.Op.DIFFERENCE);
  924. drawJitteredShadow(c);
  925. }
  926. c.restoreToCount(saveCount);
  927. }
  928. c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, outerCirclePaint);
  929.  
  930. targetCirclePaint.setAlpha(targetCircleAlpha);
  931. if (targetCirclePulseAlpha > 0) {
  932. targetCirclePulsePaint.setAlpha(targetCirclePulseAlpha);
  933. c.drawCircle(targetBounds.centerX(), targetBounds.centerY(),
  934. targetCirclePulseRadius, targetCirclePulsePaint);
  935. }
  936. c.drawCircle(targetBounds.centerX(), targetBounds.centerY(),
  937. targetCircleRadius, targetCirclePaint);
  938.  
  939. saveCount = c.save();
  940. {
  941. c.translate(textBounds.left, textBounds.top);
  942. titlePaint.setAlpha(textAlpha);
  943. if (titleLayout != null) {
  944. titleLayout.draw(c);
  945. }
  946.  
  947. if (descriptionLayout != null && titleLayout != null) {
  948. c.translate(0, titleLayout.getHeight() + TEXT_SPACING);
  949. descriptionPaint.setAlpha((int) (target.descriptionTextAlpha * textAlpha));
  950. descriptionLayout.draw(c);
  951. }
  952. }
  953. c.restoreToCount(saveCount);
  954.  
  955. saveCount = c.save();
  956. {
  957. if (tintedTarget != null) {
  958. c.translate(targetBounds.centerX() - tintedTarget.getWidth() / 2,
  959. targetBounds.centerY() - tintedTarget.getHeight() / 2);
  960. c.drawBitmap(tintedTarget, 0, 0, targetCirclePaint);
  961. } else if (target.icon != null) {
  962. c.translate(targetBounds.centerX() - target.icon.getBounds().width() / 2,
  963. targetBounds.centerY() - target.icon.getBounds().height() / 2);
  964. target.icon.setAlpha(targetCirclePaint.getAlpha());
  965. target.icon.draw(c);
  966. }
  967. }
  968. c.restoreToCount(saveCount);
  969.  
  970. if (debug) {
  971. drawDebugInformation(c);
  972. }
  973. }
  974.  
  975. @Override
  976. public boolean onTouchEvent(MotionEvent e) {
  977. lastTouchX = e.getX();
  978. lastTouchY = e.getY();
  979. return super.onTouchEvent(e);
  980. }
  981.  
  982. @Override
  983. public boolean onKeyDown(int keyCode, KeyEvent event) {
  984. if (isVisible() && cancelable && keyCode == KeyEvent.KEYCODE_BACK) {
  985. event.startTracking();
  986. return true;
  987. }
  988.  
  989. return false;
  990. }
  991.  
  992. @Override
  993. public boolean onKeyUp(int keyCode, KeyEvent event) {
  994. if (isVisible() && isInteractable && cancelable
  995. && keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) {
  996. isInteractable = false;
  997.  
  998. if (listener != null) {
  999. listener.onTargetCancel(this);
  1000. } else {
  1001. new Listener().onTargetCancel(this);
  1002. }
  1003.  
  1004. return true;
  1005. }
  1006.  
  1007. return false;
  1008. }
  1009.  
  1010. /**
  1011. * Dismiss this view
  1012. * @param tappedTarget If the user tapped the target or not
  1013. * (results in different dismiss animations)
  1014. */
  1015. public void dismiss(boolean tappedTarget) {
  1016. isDismissing = true;
  1017. pulseAnimation.cancel();
  1018. expandAnimation.cancel();
  1019. if (tappedTarget) {
  1020. dismissConfirmAnimation.start();
  1021. } else {
  1022. dismissAnimation.start();
  1023. }
  1024. }
  1025.  
  1026. /** Specify whether to draw a wireframe around the view, useful for debugging **/
  1027. public void setDrawDebug(boolean status) {
  1028. if (debug != status) {
  1029. debug = status;
  1030. postInvalidate();
  1031. }
  1032. }
  1033.  
  1034. /** Returns whether this view is visible or not **/
  1035. public boolean isVisible() {
  1036. return !isDismissed && visible;
  1037. }
  1038.  
  1039. void drawJitteredShadow(Canvas c) {
  1040. final float baseAlpha = 0.20f * outerCircleAlpha;
  1041. outerCircleShadowPaint.setStyle(Paint.Style.FILL_AND_STROKE);
  1042. outerCircleShadowPaint.setAlpha((int) baseAlpha);
  1043. c.drawCircle(outerCircleCenter[0], outerCircleCenter[1] + SHADOW_DIM, outerCircleRadius, outerCircleShadowPaint);
  1044. outerCircleShadowPaint.setStyle(Paint.Style.STROKE);
  1045. final int numJitters = 7;
  1046. for (int i = numJitters - 1; i > 0; --i) {
  1047. outerCircleShadowPaint.setAlpha((int) ((i / (float) numJitters) * baseAlpha));
  1048. c.drawCircle(outerCircleCenter[0], outerCircleCenter[1] + SHADOW_DIM ,
  1049. outerCircleRadius + (numJitters - i) * SHADOW_JITTER_DIM , outerCircleShadowPaint);
  1050. }
  1051. }
  1052.  
  1053. void drawDebugInformation(Canvas c) {
  1054. if (debugPaint == null) {
  1055. debugPaint = new Paint();
  1056. debugPaint.setARGB(255, 255, 0, 0);
  1057. debugPaint.setStyle(Paint.Style.STROKE);
  1058. debugPaint.setStrokeWidth(UiUtil.dp(getContext(), 1));
  1059. }
  1060.  
  1061. if (debugTextPaint == null) {
  1062. debugTextPaint = new TextPaint();
  1063. debugTextPaint.setColor(0xFFFF0000);
  1064. debugTextPaint.setTextSize(UiUtil.sp(getContext(), 16));
  1065. }
  1066.  
  1067. // Draw wireframe
  1068. debugPaint.setStyle(Paint.Style.STROKE);
  1069. c.drawRect(textBounds, debugPaint);
  1070. c.drawRect(targetBounds, debugPaint);
  1071. c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], 10, debugPaint);
  1072. c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], calculatedOuterCircleRadius - CIRCLE_PADDING, debugPaint);
  1073. c.drawCircle(targetBounds.centerX(), targetBounds.centerY(), TARGET_RADIUS + TARGET_PADDING, debugPaint);
  1074.  
  1075. // Draw positions and dimensions
  1076. debugPaint.setStyle(Paint.Style.FILL);
  1077. final String debugText =
  1078. "Text bounds: " + textBounds.toShortString() + "n" +
  1079. "Target bounds: " + targetBounds.toShortString() + "n" +
  1080. "Center: " + outerCircleCenter[0] + " " + outerCircleCenter[1] + "n" +
  1081. "View size: " + getWidth() + " " + getHeight() + "n" +
  1082. "Target bounds: " + targetBounds.toShortString();
  1083.  
  1084. if (debugStringBuilder == null) {
  1085. debugStringBuilder = new SpannableStringBuilder(debugText);
  1086. } else {
  1087. debugStringBuilder.clear();
  1088. debugStringBuilder.append(debugText);
  1089. }
  1090.  
  1091. if (debugLayout == null) {
  1092. debugLayout = new DynamicLayout(debugText, debugTextPaint, getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
  1093. }
  1094.  
  1095. final int saveCount = c.save();
  1096. {
  1097. debugPaint.setARGB(220, 0, 0, 0);
  1098. c.translate(0.0f, topBoundary);
  1099. c.drawRect(0.0f, 0.0f, debugLayout.getWidth(), debugLayout.getHeight(), debugPaint);
  1100. debugPaint.setARGB(255, 255, 0, 0);
  1101. debugLayout.draw(c);
  1102. }
  1103. c.restoreToCount(saveCount);
  1104. }
  1105.  
  1106. void drawTintedTarget() {
  1107. final android.graphics.drawable.Drawable icon = target.icon;
  1108. if (!shouldTintTarget || icon == null) {
  1109. tintedTarget = null;
  1110. return;
  1111. }
  1112.  
  1113. if (tintedTarget != null) return;
  1114.  
  1115. tintedTarget = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(),
  1116. Bitmap.Config.ARGB_8888);
  1117. final Canvas canvas = new Canvas(tintedTarget);
  1118. icon.setColorFilter(new PorterDuffColorFilter(
  1119. outerCirclePaint.getColor(), PorterDuff.Mode.SRC_ATOP));
  1120. icon.draw(canvas);
  1121. icon.setColorFilter(null);
  1122. }
  1123.  
  1124. void updateTextLayouts() {
  1125. final int textWidth = Math.min(getWidth(), TEXT_MAX_WIDTH) - TEXT_PADDING * 2;
  1126. if (textWidth <= 0) {
  1127. return;
  1128. }
  1129.  
  1130. titleLayout = new StaticLayout(title, titlePaint, textWidth,
  1131. Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
  1132.  
  1133. if (description != null) {
  1134. descriptionLayout = new StaticLayout(description, descriptionPaint, textWidth,
  1135. Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
  1136. } else {
  1137. descriptionLayout = null;
  1138. }
  1139. }
  1140.  
  1141. float halfwayLerp(float lerp) {
  1142. if (lerp < 0.5f) {
  1143. return lerp / 0.5f;
  1144. }
  1145.  
  1146. return (1.0f - lerp) / 0.5f;
  1147. }
  1148.  
  1149. float delayedLerp(float lerp, float threshold) {
  1150. if (lerp < threshold) {
  1151. return 0.0f;
  1152. }
  1153.  
  1154. return (lerp - threshold) / (1.0f - threshold);
  1155. }
  1156.  
  1157. void calculateDimensions() {
  1158. textBounds = getTextBounds();
  1159. outerCircleCenter = getOuterCircleCenterPoint();
  1160. calculatedOuterCircleRadius = getOuterCircleRadius(outerCircleCenter[0], outerCircleCenter[1], textBounds, targetBounds);
  1161. }
  1162.  
  1163. void calculateDrawingBounds() {
  1164. if (outerCircleCenter == null) {
  1165. // Called dismiss before we got a chance to display the tap target
  1166. // So we have no center -> cant determine the drawing bounds
  1167. return;
  1168. }
  1169. drawingBounds.left = (int) Math.max(0, outerCircleCenter[0] - outerCircleRadius);
  1170. drawingBounds.top = (int) Math.min(0, outerCircleCenter[1] - outerCircleRadius);
  1171. drawingBounds.right = (int) Math.min(getWidth(),
  1172. outerCircleCenter[0] + outerCircleRadius + CIRCLE_PADDING);
  1173. drawingBounds.bottom = (int) Math.min(getHeight(),
  1174. outerCircleCenter[1] + outerCircleRadius + CIRCLE_PADDING);
  1175. }
  1176.  
  1177. int getOuterCircleRadius(int centerX, int centerY, Rect textBounds, Rect targetBounds) {
  1178. final int targetCenterX = targetBounds.centerX();
  1179. final int targetCenterY = targetBounds.centerY();
  1180. final int expandedRadius = (int) (1.1f * TARGET_RADIUS);
  1181. final Rect expandedBounds = new Rect(targetCenterX, targetCenterY, targetCenterX, targetCenterY);
  1182. expandedBounds.inset(-expandedRadius, -expandedRadius);
  1183.  
  1184. final int textRadius = maxDistanceToPoints(centerX, centerY, textBounds);
  1185. final int targetRadius = maxDistanceToPoints(centerX, centerY, expandedBounds);
  1186. return Math.max(textRadius, targetRadius) + CIRCLE_PADDING;
  1187. }
  1188.  
  1189. Rect getTextBounds() {
  1190. final int totalTextHeight = getTotalTextHeight();
  1191. final int totalTextWidth = getTotalTextWidth();
  1192.  
  1193. final int possibleTop = targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight;
  1194. final int top;
  1195. if (possibleTop > topBoundary) {
  1196. top = possibleTop;
  1197. } else {
  1198. top = targetBounds.centerY() + TARGET_RADIUS + TARGET_PADDING;
  1199. }
  1200.  
  1201. final int relativeCenterDistance = (getWidth() / 2) - targetBounds.centerX();
  1202. final int bias = relativeCenterDistance < 0 ? -TEXT_POSITIONING_BIAS : TEXT_POSITIONING_BIAS;
  1203. final int left = Math.max(TEXT_PADDING, targetBounds.centerX() - bias - totalTextWidth);
  1204. final int right = Math.min(getWidth() - TEXT_PADDING, left + totalTextWidth);
  1205. return new Rect(left, top, right, top + totalTextHeight);
  1206. }
  1207.  
  1208. int[] getOuterCircleCenterPoint() {
  1209. if (inGutter(targetBounds.centerY())) {
  1210. return new int[]{targetBounds.centerX(), targetBounds.centerY()};
  1211. }
  1212.  
  1213. final int targetRadius = Math.max(targetBounds.width(), targetBounds.height()) / 2 + TARGET_PADDING;
  1214. final int totalTextHeight = getTotalTextHeight();
  1215.  
  1216. final boolean onTop = targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight > 0;
  1217.  
  1218. final int left = Math.min(textBounds.left, targetBounds.left - targetRadius);
  1219. final int right = Math.max(textBounds.right, targetBounds.right + targetRadius);
  1220. final int titleHeight = titleLayout == null ? 0 : titleLayout.getHeight();
  1221. final int centerY = onTop ?
  1222. targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight + titleHeight
  1223. :
  1224. targetBounds.centerY() + TARGET_RADIUS + TARGET_PADDING + titleHeight;
  1225.  
  1226. return new int[] { (left + right) / 2, centerY };
  1227. }
  1228.  
  1229. int getTotalTextHeight() {
  1230. if (titleLayout == null) {
  1231. return 0;
  1232. }
  1233.  
  1234. if (descriptionLayout == null) {
  1235. return titleLayout.getHeight() + TEXT_SPACING;
  1236. }
  1237.  
  1238. return titleLayout.getHeight() + descriptionLayout.getHeight() + TEXT_SPACING;
  1239. }
  1240.  
  1241. int getTotalTextWidth() {
  1242. if (titleLayout == null) {
  1243. return 0;
  1244. }
  1245.  
  1246. if (descriptionLayout == null) {
  1247. return titleLayout.getWidth();
  1248. }
  1249.  
  1250. return Math.max(titleLayout.getWidth(), descriptionLayout.getWidth());
  1251. }
  1252. boolean inGutter(int y) {
  1253. if (bottomBoundary > 0) {
  1254. return y < GUTTER_DIM || y > bottomBoundary - GUTTER_DIM;
  1255. } else {
  1256. return y < GUTTER_DIM || y > getHeight() - GUTTER_DIM;
  1257. }
  1258. }
  1259. int maxDistanceToPoints(int x1, int y1, Rect bounds) {
  1260. final double tl = distance(x1, y1, bounds.left, bounds.top);
  1261. final double tr = distance(x1, y1, bounds.right, bounds.top);
  1262. final double bl = distance(x1, y1, bounds.left, bounds.bottom);
  1263. final double br = distance(x1, y1, bounds.right, bounds.bottom);
  1264. return (int) Math.max(tl, Math.max(tr, Math.max(bl, br)));
  1265. }
  1266. double distance(int x1, int y1, int x2, int y2) {
  1267. return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
  1268. }
  1269. void invalidateViewAndOutline(Rect bounds) {
  1270. invalidate(bounds);
  1271. if (outlineProvider != null && Build.VERSION.SDK_INT >= 21) {
  1272. invalidateOutline();
  1273. }
  1274. }
  1275. }
  1276. static class ViewUtil {
  1277.  
  1278. ViewUtil() {}
  1279.  
  1280. private static boolean isLaidOut(View view) {
  1281. return true;
  1282. }
  1283. static void onLaidOut(final View view, final Runnable runnable) {
  1284. if (isLaidOut(view)) {
  1285. runnable.run();
  1286. return;
  1287. }
  1288. final ViewTreeObserver observer = view.getViewTreeObserver();
  1289. observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
  1290. @Override
  1291. public void onGlobalLayout() {
  1292. final ViewTreeObserver trueObserver;
  1293. if (observer.isAlive()) {
  1294. trueObserver = observer;
  1295. } else {
  1296. trueObserver = view.getViewTreeObserver();
  1297. }
  1298. removeOnGlobalLayoutListener(trueObserver, this);
  1299. runnable.run();
  1300. }
  1301. });
  1302. }
  1303. @SuppressWarnings("deprecation")
  1304. static void removeOnGlobalLayoutListener(ViewTreeObserver observer,
  1305. ViewTreeObserver.OnGlobalLayoutListener listener) {
  1306. if (Build.VERSION.SDK_INT >= 16) {
  1307. observer.removeOnGlobalLayoutListener(listener);
  1308. } else {
  1309. observer.removeGlobalOnLayoutListener(listener);
  1310. }
  1311. }
  1312. static void removeView(ViewManager parent, View child) {
  1313. if (parent == null || child == null) {
  1314. return;
  1315. }
  1316. try {
  1317. parent.removeView(child);
  1318. } catch (Exception ignored) {
  1319. }
  1320. }
  1321. }
  1322. static class ViewTapTarget extends TapTarget {
  1323. final View view;
  1324.  
  1325. ViewTapTarget(View view, CharSequence title, CharSequence description) {
  1326. super(title, description);
  1327. if (view == null) {
  1328. throw new IllegalArgumentException("Given null view to target");
  1329. }
  1330. this.view = view;
  1331. }
  1332.  
  1333. @Override
  1334. public void onReady(final Runnable runnable) {
  1335. ViewUtil.onLaidOut(view, new Runnable() {
  1336. @Override
  1337. public void run() {
  1338. // Cache bounds
  1339. final int[] location = new int[2];
  1340. view.getLocationOnScreen(location);
  1341. bounds = new Rect(location[0], location[1],
  1342. location[0] + view.getWidth(), location[1] + view.getHeight());
  1343.  
  1344. if (icon == null && view.getWidth() > 0 && view.getHeight() > 0) {
  1345. final Bitmap viewBitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
  1346. final Canvas canvas = new Canvas(viewBitmap);
  1347. view.draw(canvas);
  1348. icon = new android.graphics.drawable.BitmapDrawable(view.getContext().getResources(), viewBitmap);
  1349. icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
  1350. }
  1351.  
  1352. runnable.run();
  1353. }
  1354. });
  1355. }
  1356. }
  1357. static class TapTargetSequence {
  1358. private final Activity activity;
  1359. private final Dialog dialog;
  1360. private final Queue<TapTarget> targets;
  1361. private boolean active;
  1362. private TapTargetView currentView;
  1363. Listener listener;
  1364. boolean considerOuterCircleCanceled;
  1365. boolean continueOnCancel;
  1366. public interface Listener {
  1367. void onSequenceFinish();
  1368. void onSequenceStep(TapTarget lastTarget, boolean targetClicked);
  1369. void onSequenceCanceled(TapTarget lastTarget);
  1370. }
  1371. public TapTargetSequence(Activity activity) {
  1372. if (activity == null) throw new IllegalArgumentException("Activity is null");
  1373. this.activity = activity;
  1374. this.dialog = null;
  1375. this.targets = new LinkedList<>();
  1376. }
  1377. public TapTargetSequence(Dialog dialog) {
  1378. if (dialog == null) throw new IllegalArgumentException("Given null Dialog");
  1379. this.dialog = dialog;
  1380. this.activity = null;
  1381. this.targets = new LinkedList<>();
  1382. }
  1383. public TapTargetSequence targets(List<TapTarget> targets) {
  1384. this.targets.addAll(targets);
  1385. return this;
  1386. }
  1387. public TapTargetSequence targets(TapTarget... targets) {
  1388. Collections.addAll(this.targets, targets);
  1389. return this;
  1390. }
  1391. public TapTargetSequence target(TapTarget target) {
  1392. this.targets.add(target);
  1393. return this;
  1394. }
  1395. public TapTargetSequence continueOnCancel(boolean status) {
  1396. this.continueOnCancel = status;
  1397. return this;
  1398. }
  1399. public TapTargetSequence considerOuterCircleCanceled(boolean status) {
  1400. this.considerOuterCircleCanceled = status;
  1401. return this;
  1402. }
  1403. public TapTargetSequence listener(Listener listener) {
  1404. this.listener = listener;
  1405. return this;
  1406. }
  1407. public void start() {
  1408. if (targets.isEmpty() || active) {
  1409. return;
  1410. }
  1411. active = true;
  1412. showNext();
  1413. }
  1414. public void startWith(int targetId) {
  1415. if (active) {
  1416. return;
  1417. }
  1418. while (targets.peek() != null && targets.peek().id() != targetId) {
  1419. targets.poll();
  1420. }
  1421. TapTarget peekedTarget = targets.peek();
  1422. if (peekedTarget == null || peekedTarget.id() != targetId) {
  1423. throw new IllegalStateException("Given target " + targetId + " not in sequence");
  1424. }
  1425. start();
  1426. }
  1427. public void startAt(int index) {
  1428. if (active) {
  1429. return;
  1430. }
  1431. if (index < 0 || index >= targets.size()) {
  1432. throw new IllegalArgumentException("Given invalid index " + index);
  1433. }
  1434. final int expectedSize = targets.size() - index;
  1435. while (targets.peek() != null && targets.size() != expectedSize) {
  1436. targets.poll();
  1437. }
  1438. if (targets.size() != expectedSize) {
  1439. throw new IllegalStateException("Given index " + index + " not in sequence");
  1440. }
  1441. start();
  1442. }
  1443. public boolean cancel() {
  1444. if (targets.isEmpty() || !active) {
  1445. return false;
  1446. }
  1447. if (currentView == null || !currentView.cancelable) {
  1448. return false;
  1449. }
  1450. currentView.dismiss(false);
  1451. active = false;
  1452. targets.clear();
  1453. if (listener != null) {
  1454. listener.onSequenceCanceled(currentView.target);
  1455. }
  1456. return true;
  1457. }
  1458. void showNext() {
  1459. try {
  1460. TapTarget tapTarget = targets.remove();
  1461. if (activity != null) {
  1462. currentView = TapTargetView.showFor(activity, tapTarget, tapTargetListener);
  1463. } else {
  1464. currentView = TapTargetView.showFor(dialog, tapTarget, tapTargetListener);
  1465. }
  1466. } catch (NoSuchElementException e) {
  1467. // No more targets
  1468. if (listener != null) {
  1469. listener.onSequenceFinish();
  1470. }
  1471. }
  1472. }
  1473. private final TapTargetView.Listener tapTargetListener = new TapTargetView.Listener() {
  1474. @Override
  1475. public void onTargetClick(TapTargetView view) {
  1476. super.onTargetClick(view);
  1477. if (listener != null) {
  1478. listener.onSequenceStep(view.target, true);
  1479. }
  1480. showNext();
  1481. }
  1482. @Override
  1483. public void onOuterCircleClick(TapTargetView view) {
  1484. if (considerOuterCircleCanceled) {
  1485. onTargetCancel(view);
  1486. }
  1487. }
  1488. @Override
  1489. public void onTargetCancel(TapTargetView view) {
  1490. super.onTargetCancel(view);
  1491. if (continueOnCancel) {
  1492. if (listener != null) {
  1493. listener.onSequenceStep(view.target, false);
  1494. }
  1495. showNext();
  1496. } else {
  1497. if (listener != null) {
  1498. listener.onSequenceCanceled(view.target);
  1499. }
  1500. }
  1501. }
  1502. };
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement