Smoke and Mirrors the Magic behind wonderful UI in Android Israel - - PowerPoint PPT Presentation

smoke and mirrors
SMART_READER_LITE
LIVE PREVIEW

Smoke and Mirrors the Magic behind wonderful UI in Android Israel - - PowerPoint PPT Presentation

Smoke and Mirrors the Magic behind wonderful UI in Android Israel Ferrer Camacho @rallat Smoke and mirrors N. Amer. the obscuring or embellishing of the truth. Android Framework RecyclerView LayoutManager getViewForPosition RecyclerView


slide-1
SLIDE 1

Smoke and Mirrors

the Magic behind wonderful UI in Android

slide-2
SLIDE 2
slide-3
SLIDE 3

Israel Ferrer Camacho @rallat

slide-4
SLIDE 4

Smoke and mirrors

  • N. Amer. the obscuring or embellishing of the

truth.

slide-5
SLIDE 5

Android Framework

slide-6
SLIDE 6
slide-7
SLIDE 7

RecyclerView

RecyclePool Adapter RecyclerView LayoutManager getViewForPosition getViewHolderByType getViewType bindViewHolder

slide-8
SLIDE 8
slide-9
SLIDE 9
slide-10
SLIDE 10

Shared Element Transitions

TransitionManager ActivityTransitionCoordinator ActivityTransitionState startExit/startEnter beginDelayedTransition

slide-11
SLIDE 11

ViewOverlay

LinearLayout ViewOverlay of the LinearLayout ViewOverlay.add( );

slide-12
SLIDE 12

ViewOverlay

LinearLayout re-layout ViewOverlay of the LinearLayout

slide-13
SLIDE 13
slide-14
SLIDE 14

/**
 * A transition listener receives notifications from a transition.
 * Notifications indicate transition lifecycle events.
 */
 public static interface TransitionListener {
 
 void onTransitionStart(Transition transition);
 
 void onTransitionEnd(Transition transition);
 
 void onTransitionCancel(Transition transition);
 
 void onTransitionPause(Transition transition);
 
 void onTransitionResume(Transition transition);
 }


slide-15
SLIDE 15

private View.OnTouchListener touchEater = new View.OnTouchListener() {
 @Override
 public boolean onTouch(View view, MotionEvent motionEvent) {
 return true;
 }
 };

slide-16
SLIDE 16

Important attributes

slide-17
SLIDE 17

ClipChildren

slide-18
SLIDE 18
slide-19
SLIDE 19

<?xml version="1.0" encoding="utf-8"?>
 <FrameLayout
 android:id="@+id/parent"
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:padding="@dimen/activity_vertical_margin">
 <FrameLayout
 android:layout_width="300dp"
 android:layout_height="300dp"
 android:background="@color/colorAccent"
 android:clipChildren="false">
 <FrameLayout
 android:layout_width="200dp"
 android:layout_height="200dp"
 android:background="@color/colorPrimary">
 <ImageView
 android:id="@+id/imageview"
 android:layout_width="100dp"
 android:layout_height="100dp"
 android:src="@drawable/profile"/>
 </FrameLayout>
 </FrameLayout>
 </FrameLayout>


slide-20
SLIDE 20

<?xml version="1.0" encoding="utf-8"?>
 <FrameLayout
 android:id="@+id/parent"
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:clipChildren=“false"
 android:padding="@dimen/activity_vertical_margin">
 <FrameLayout
 android:layout_width="300dp"
 android:layout_height="300dp"
 android:background="@color/colorAccent"
 android:clipChildren="false">
 <FrameLayout
 android:layout_width="200dp"
 android:layout_height="200dp"
 android:background="@color/colorPrimary">
 <ImageView
 android:id="@+id/imageview"
 android:layout_width="100dp"
 android:layout_height="100dp"
 android:src="@drawable/profile"/>
 </FrameLayout>
 </FrameLayout>
 </FrameLayout>

slide-21
SLIDE 21

ClipPadding

slide-22
SLIDE 22
slide-23
SLIDE 23

Utils

public static void disableParentsClip(@NonNull View view) {
 while (view.getParent() != null && view.getParent() instanceof ViewGroup) {
 ViewGroup viewGroup = (ViewGroup) view.getParent();
 viewGroup.setClipChildren(false);
 viewGroup.setClipToPadding(false);
 view = viewGroup;
 }
 }
 
 public static void enableParentsClip(@NonNull View view) {
 while (view.getParent() != null && view.getParent() instanceof ViewGroup) {
 ViewGroup viewGroup = (ViewGroup) view.getParent();
 viewGroup.setClipChildren(true);
 viewGroup.setClipToPadding(true);
 view = viewGroup;
 }


slide-24
SLIDE 24
slide-25
SLIDE 25
slide-26
SLIDE 26
slide-27
SLIDE 27
slide-28
SLIDE 28
slide-29
SLIDE 29
slide-30
SLIDE 30
slide-31
SLIDE 31
slide-32
SLIDE 32
slide-33
SLIDE 33

Smoke and mirrors

Lesson: Almost anything fast enough will look good

slide-34
SLIDE 34
slide-35
SLIDE 35
slide-36
SLIDE 36
slide-37
SLIDE 37

Smoke and mirrors

Lesson: Fake it until you make it.

slide-38
SLIDE 38

Demo

slide-39
SLIDE 39
slide-40
SLIDE 40
slide-41
SLIDE 41
slide-42
SLIDE 42

Pivot

slide-43
SLIDE 43
slide-44
SLIDE 44

/**


* Sets the x location of the point around which the view * is {@link #setRotation(float) rotated} and * {@link #setScaleX(float) scaled}.
 * By default, the pivot point is centered on the object. */

smallRecyclerView.setPivotX(0); smallRecyclerView.setPivotY(0); mediumRecyclerView.setPivotX(0);
 mediumRecyclerView.setPivotY(0);

slide-45
SLIDE 45

smallRecyclerView.setAdapter(smallAdapter); 
 mediumRecyclerView.setAdapter(mediumAdapter);
 mediumRecyclerView.setVisibility(INVISIBLE);


slide-46
SLIDE 46

ItemTouchListenerDispatcher dispatcher = new ItemTouchListenerDispatcher(this, galleryGestureDetector, fullScreenGestureDetector); 
 smallRecyclerView.addOnItemTouchListener(onItemTouchListener);
 mediumRecyclerView.addOnItemTouchListener(onItemTouchListener);

slide-47
SLIDE 47

public class ItemTouchListenerDispatcher implements RecyclerView.OnItemTouchListener {
 …
 @Override
 public void onTouchEvent(RecyclerView rv, MotionEvent e) {
 currentSpan = getSpan(e);
 switch (rv.getId()) {
 case R.id.mediumRecyclerView: {
 if (currentSpan < 0) {
 galleryGestureDetector.onTouchEvent(e);
 } else if (currentSpan == 0) {
 final View childViewUnder = rv.findChildViewUnder(e.getX(), e.getY());
 if (childViewUnder != null) {
 childViewUnder.performClick();
 }
 }
 break;
 }
 case R.id.smallRecyclerView: {
 galleryGestureDetector.onTouchEvent(e);
 break;
 }
 default: {
 break;
 }
 
 }
 }
 …

slide-48
SLIDE 48

public class ItemTouchListenerDispatcher implements RecyclerView.OnItemTouchListener {
 …
 @Override
 public void onTouchEvent(RecyclerView rv, MotionEvent e) {
 currentSpan = getSpan(e);
 switch (rv.getId()) {
 case R.id.mediumRecyclerView: {
 if (currentSpan < 0) {
 galleryGestureDetector.onTouchEvent(e);
 } else if (currentSpan == 0) {
 final View childViewUnder = rv.findChildViewUnder(e.getX(), e.getY());
 if (childViewUnder != null) {
 childViewUnder.performClick();
 }
 }
 break;
 }
 case R.id.smallRecyclerView: {
 galleryGestureDetector.onTouchEvent(e);
 break;
 }
 default: {
 break;
 }
 
 }
 }
 …

slide-49
SLIDE 49

public class ItemTouchListenerDispatcher implements RecyclerView.OnItemTouchListener {
 …
 @Override
 public void onTouchEvent(RecyclerView rv, MotionEvent e) {
 currentSpan = getSpan(e);
 switch (rv.getId()) {
 case R.id.mediumRecyclerView: {
 if (currentSpan < 0) {
 galleryGestureDetector.onTouchEvent(e);
 } else if (currentSpan == 0) {
 final View childViewUnder = rv.findChildViewUnder(e.getX(), e.getY());
 if (childViewUnder != null) {
 childViewUnder.performClick();
 }
 }
 break;
 }
 case R.id.smallRecyclerView: {
 galleryGestureDetector.onTouchEvent(e);
 break;
 }
 default: {
 break;
 }
 
 }
 }
 …

slide-50
SLIDE 50

public class ItemTouchListenerDispatcher implements RecyclerView.OnItemTouchListener {
 …
 @Override
 public void onTouchEvent(RecyclerView rv, MotionEvent e) {
 currentSpan = getSpan(e);
 switch (rv.getId()) {
 case R.id.mediumRecyclerView: {
 if (currentSpan < 0) {
 galleryGestureDetector.onTouchEvent(e);
 } else if (currentSpan == 0) {
 final View childViewUnder = rv.findChildViewUnder(e.getX(), e.getY());
 if (childViewUnder != null) {
 childViewUnder.performClick();
 }
 }
 break;
 }
 case R.id.smallRecyclerView: {
 galleryGestureDetector.onTouchEvent(e);
 break;
 }
 default: {
 break;
 }
 
 }
 }
 …

slide-51
SLIDE 51

Scaling with gesture

slide-52
SLIDE 52
slide-53
SLIDE 53

public interface OnScaleGestureListener {
 /**
 * Responds to scaling events for a gesture in progress.
 * Reported by pointer motion.
 */
 public boolean onScale(ScaleGestureDetector detector);
 
 /**
 * Responds to the beginning of a scaling gesture. Reported by
 * new pointers going down.
 */
 public boolean onScaleBegin(ScaleGestureDetector detector);
 
 /**
 * Responds to the end of a scale gesture. Reported by existing
 * pointers going up.
 
 * @param detector The detector reporting the event - use this to
 * retrieve extended info about event state.
 */
 public void onScaleEnd(ScaleGestureDetector detector);
 }

slide-54
SLIDE 54

Scale begin

@Override
 public boolean onScaleBegin(@NonNull ScaleGestureDetector detector) {
 mediumRecyclerView.setVisibility(View.VISIBLE);
 smallRecyclerView.setVisibility(View.VISIBLE);
 return true;
 }

slide-55
SLIDE 55

During Scale

@Override
 public boolean onScale(@NonNull ScaleGestureDetector detector) {
 if (gestureTolerance(detector)) {
 //small
 scaleFactor *= detector.getScaleFactor();
 scaleFactor = Math.max(1f, Math.min(scaleFactor, SMALL_MAX_SCALE_FACTOR));
 isInProgress = scaleFactor > 1;
 smallRecyclerView.setScaleX(scaleFactor);
 smallRecyclerView.setScaleY(scaleFactor);
 
 //medium
 scaleFactorMedium *= detector.getScaleFactor();
 scaleFactorMedium = Math.max(0.8f, Math.min(scaleFactorMedium, 1f));
 mediumRecyclerView.setScaleX(scaleFactorMedium);
 mediumRecyclerView.setScaleY(scaleFactorMedium);
 
 //alpha
 mediumRecyclerView.setAlpha((scaleFactor - 1) / (0.25f));
 smallRecyclerView.setAlpha(1 - (scaleFactor - 1) / (0.25f));
 
 }
 return true;
 }

slide-56
SLIDE 56

During Scale

@Override
 public boolean onScale(@NonNull ScaleGestureDetector detector) {
 if (gestureTolerance(detector)) {
 //small
 scaleFactor *= detector.getScaleFactor();
 scaleFactor = Math.max(1f, Math.min(scaleFactor, SMALL_MAX_SCALE_FACTOR));
 isInProgress = scaleFactor > 1;
 smallRecyclerView.setScaleX(scaleFactor);
 smallRecyclerView.setScaleY(scaleFactor);
 
 //medium
 scaleFactorMedium *= detector.getScaleFactor();
 scaleFactorMedium = Math.max(0.8f, Math.min(scaleFactorMedium, 1f));
 mediumRecyclerView.setScaleX(scaleFactorMedium);
 mediumRecyclerView.setScaleY(scaleFactorMedium);
 
 //alpha
 mediumRecyclerView.setAlpha((scaleFactor - 1) / (0.25f));
 smallRecyclerView.setAlpha(1 - (scaleFactor - 1) / (0.25f));
 
 }
 return true;
 }

slide-57
SLIDE 57

During Scale

@Override
 public boolean onScale(@NonNull ScaleGestureDetector detector) {
 if (gestureTolerance(detector)) {
 //small
 scaleFactor *= detector.getScaleFactor();
 scaleFactor = Math.max(1f, Math.min(scaleFactor, SMALL_MAX_SCALE_FACTOR));
 isInProgress = scaleFactor > 1;
 smallRecyclerView.setScaleX(scaleFactor);
 smallRecyclerView.setScaleY(scaleFactor);
 
 //medium
 scaleFactorMedium *= detector.getScaleFactor();
 scaleFactorMedium = Math.max(0.8f, Math.min(scaleFactorMedium, 1f));
 mediumRecyclerView.setScaleX(scaleFactorMedium);
 mediumRecyclerView.setScaleY(scaleFactorMedium);
 
 //alpha
 mediumRecyclerView.setAlpha((scaleFactor - 1) / (0.25f));
 smallRecyclerView.setAlpha(1 - (scaleFactor - 1) / (0.25f));
 
 }
 return true;
 }

slide-58
SLIDE 58

During Scale

@Override
 public boolean onScale(@NonNull ScaleGestureDetector detector) {
 if (gestureTolerance(detector)) {
 //small
 scaleFactor *= detector.getScaleFactor();
 scaleFactor = Math.max(1f, Math.min(scaleFactor, SMALL_MAX_SCALE_FACTOR));
 isInProgress = scaleFactor > 1;
 smallRecyclerView.setScaleX(scaleFactor);
 smallRecyclerView.setScaleY(scaleFactor);
 
 //medium
 scaleFactorMedium *= detector.getScaleFactor();
 scaleFactorMedium = Math.max(0.8f, Math.min(scaleFactorMedium, 1f));
 mediumRecyclerView.setScaleX(scaleFactorMedium);
 mediumRecyclerView.setScaleY(scaleFactorMedium);
 
 //alpha
 mediumRecyclerView.setAlpha((scaleFactor - 1) / (0.25f));
 smallRecyclerView.setAlpha(1 - (scaleFactor - 1) / (0.25f));
 
 }
 return true;
 }

slide-59
SLIDE 59

Scale ends

@Override
 public void onScaleEnd(@NonNull ScaleGestureDetector detector) {
 if (IsScaleInProgress()) {
 if (scaleFactor < TRANSITION_BOUNDARY) {
 transitionFromMediumToSmall();
 scaleFactor = 0;
 scaleFactorMedium = 0;
 } else {
 transitionFromSmallToMedium();
 scaleFactor = SMALL_MAX_SCALE_FACTOR;
 scaleFactorMedium = 1f;
 }
 }
 }

slide-60
SLIDE 60

Scale ends

@Override
 public void onScaleEnd(@NonNull ScaleGestureDetector detector) {
 if (IsScaleInProgress()) {
 if (scaleFactor < TRANSITION_BOUNDARY) {
 transitionFromMediumToSmall();
 scaleFactor = 0;
 scaleFactorMedium = 0;
 } else {
 transitionFromSmallToMedium();
 scaleFactor = SMALL_MAX_SCALE_FACTOR;
 scaleFactorMedium = 1f;
 }
 }
 }

slide-61
SLIDE 61

Scale ends

@Override
 public void onScaleEnd(@NonNull ScaleGestureDetector detector) {
 if (IsScaleInProgress()) {
 if (scaleFactor < TRANSITION_BOUNDARY) {
 transitionFromMediumToSmall();
 scaleFactor = 0;
 scaleFactorMedium = 0;
 } else {
 transitionFromSmallToMedium();
 scaleFactor = SMALL_MAX_SCALE_FACTOR;
 scaleFactorMedium = 1f;
 }
 }
 }

slide-62
SLIDE 62

Scale ends

@Override
 public void onScaleEnd(@NonNull ScaleGestureDetector detector) {
 if (IsScaleInProgress()) {
 if (scaleFactor < TRANSITION_BOUNDARY) {
 transitionFromMediumToSmall();
 scaleFactor = 0;
 scaleFactorMedium = 0;
 } else {
 transitionFromSmallToMedium();
 scaleFactor = SMALL_MAX_SCALE_FACTOR;
 scaleFactorMedium = 1f;
 }
 }
 }

slide-63
SLIDE 63
slide-64
SLIDE 64
slide-65
SLIDE 65

Recycler View elevation

  • Custom LayoutManager
  • RecyclerView.ChildDrawingOrderCallback
  • Easier! Fake it with ViewOverlay!
slide-66
SLIDE 66

Transition to full screen


 @Override
 public void onClick(@NonNull final View itemView) {
 ViewGroupOverlay overlay = fullScreenContainer.getOverlay();


  • verlay.clear();

  • verlay.add(itemView);


fullScreenContainer.setBackgroundColor(TRANSPARENT);
 fullScreenContainer.setVisibility(View.VISIBLE);
 itemView.animate() .x(DELTA_TO_CENTER_X).y(DELTA_TO_CENTER_Y)
 .scaleX(DELTA_SCALE).scaleY(DELTA_SCALE) .withEndAction(setTransitionToRecyclerView()).start();
 }
 }

slide-67
SLIDE 67

Transition to full screen


 @Override
 public void onClick(@NonNull final View itemView) {
 ViewGroupOverlay overlay = fullScreenContainer.getOverlay();


  • verlay.clear();

  • verlay.add(itemView);


fullScreenContainer.setBackgroundColor(TRANSPARENT);
 fullScreenContainer.setVisibility(View.VISIBLE);
 itemView.animate() .x(DELTA_TO_CENTER_X).y(DELTA_TO_CENTER_Y)
 .scaleX(DELTA_SCALE).scaleY(DELTA_SCALE) .withEndAction(setTransitionToRecyclerView()).start();
 }
 }

slide-68
SLIDE 68

Transition to full screen


 @Override
 public void onClick(@NonNull final View itemView) {
 ViewGroupOverlay overlay = fullScreenContainer.getOverlay();


  • verlay.clear();

  • verlay.add(itemView);


fullScreenContainer.setBackgroundColor(TRANSPARENT);
 fullScreenContainer.setVisibility(View.VISIBLE);
 itemView.animate() .x(DELTA_TO_CENTER_X).y(DELTA_TO_CENTER_Y)
 .scaleX(DELTA_SCALE).scaleY(DELTA_SCALE) .withEndAction(setTransitionToRecyclerView()).start();
 }
 }

slide-69
SLIDE 69

Transition to full screen


 @Override
 public void onClick(@NonNull final View itemView) {
 ViewGroupOverlay overlay = fullScreenContainer.getOverlay();


  • verlay.clear();

  • verlay.add(itemView);


fullScreenContainer.setBackgroundColor(TRANSPARENT);
 fullScreenContainer.setVisibility(View.VISIBLE);
 itemView.animate() .x(DELTA_TO_CENTER_X).y(DELTA_TO_CENTER_Y)
 .scaleX(DELTA_SCALE).scaleY(DELTA_SCALE) .withEndAction(setTransitionToRecyclerView()).start();
 }
 }

slide-70
SLIDE 70

Transition to full screen


 @Override
 public void onClick(@NonNull final View itemView) {
 ViewGroupOverlay overlay = fullScreenContainer.getOverlay();


  • verlay.clear();

  • verlay.add(itemView);


fullScreenContainer.setBackgroundColor(TRANSPARENT);
 fullScreenContainer.setVisibility(View.VISIBLE);
 itemView.animate() .x(DELTA_TO_CENTER_X).y(DELTA_TO_CENTER_Y)
 .scaleX(DELTA_SCALE).scaleY(DELTA_SCALE) .withEndAction(setTransitionToRecyclerView()).start();
 }
 }

slide-71
SLIDE 71

Transition to full screen

private Runnable setTransitionToRecyclerView() {
 return new Runnable() {
 @Override
 public void run() {
 fullScreenContainer.setBackgroundColor(BLACK);
 fullScreenContainer.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v) {


  • verlay.add(itemView);


fullScreenContainer.setBackgroundColor(TRANSPARENT);
 itemView.animate().x(originX).y(originY) .scaleY(1).scaleX(1).withEndAction(
 new Runnable() {
 @Override
 public void run() {


  • verlay.remove(itemView);


fullScreenContainer.setVisibility(View.GONE);
 }
 }).start();
 }
 });
 }
 };
 }

slide-72
SLIDE 72

Transition to full screen

private Runnable setTransitionToRecyclerView() {
 return new Runnable() {
 @Override
 public void run() {
 fullScreenContainer.setBackgroundColor(BLACK);
 fullScreenContainer.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v) {


  • verlay.add(itemView);


fullScreenContainer.setBackgroundColor(TRANSPARENT);
 itemView.animate().x(originX).y(originY) .scaleY(1).scaleX(1).withEndAction(
 new Runnable() {
 @Override
 public void run() {


  • verlay.remove(itemView);


fullScreenContainer.setVisibility(View.GONE);
 }
 }).start();
 }
 });
 }
 };
 }

slide-73
SLIDE 73

Transition to full screen

private Runnable setTransitionToRecyclerView() {
 return new Runnable() {
 @Override
 public void run() {
 fullScreenContainer.setBackgroundColor(BLACK);
 fullScreenContainer.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v) {


  • verlay.add(itemView);


fullScreenContainer.setBackgroundColor(TRANSPARENT);
 itemView.animate().x(originX).y(originY) .scaleY(1).scaleX(1).withEndAction(
 new Runnable() {
 @Override
 public void run() {


  • verlay.remove(itemView);


fullScreenContainer.setVisibility(View.GONE);
 }
 }).start();
 }
 });
 }
 };
 }

slide-74
SLIDE 74

Transition to full screen

private Runnable setTransitionToRecyclerView() {
 return new Runnable() {
 @Override
 public void run() {
 fullScreenContainer.setBackgroundColor(BLACK);
 fullScreenContainer.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v) {


  • verlay.add(itemView);


fullScreenContainer.setBackgroundColor(TRANSPARENT);
 itemView.animate().x(originX).y(originY) .scaleY(1).scaleX(1).withEndAction(
 new Runnable() {
 @Override
 public void run() {


  • verlay.remove(itemView);


fullScreenContainer.setVisibility(View.GONE);
 }
 }).start();
 }
 });
 }
 };
 }

slide-75
SLIDE 75
slide-76
SLIDE 76
slide-77
SLIDE 77

Magic tricks

  • ViewOverlay for animations
  • Single Activity allows control of the transition by the

user touch

  • Fast animations to hide implementation details and

make it look cool

  • ClipPadding, ClipChildren to draw over

parents&paddings

slide-78
SLIDE 78

Thank you!

@rallat github.com/rallat/smokeAndMirrors

slide-79
SLIDE 79

github.com/nickbutcher/plaid

More examples

slide-80
SLIDE 80