Smoke and Mirrors
the Magic behind wonderful UI in Android
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
the Magic behind wonderful UI in Android
Israel Ferrer Camacho @rallat
truth.
RecyclePool Adapter RecyclerView LayoutManager getViewForPosition getViewHolderByType getViewType bindViewHolder
TransitionManager ActivityTransitionCoordinator ActivityTransitionState startExit/startEnter beginDelayedTransition
LinearLayout ViewOverlay of the LinearLayout ViewOverlay.add( );
LinearLayout re-layout ViewOverlay of the LinearLayout
/** * 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); }
private View.OnTouchListener touchEater = new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { return true; } };
<?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>
<?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>
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; }
Lesson: Almost anything fast enough will look good
Lesson: Fake it until you make it.
/**
* 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);
smallRecyclerView.setAdapter(smallAdapter); mediumRecyclerView.setAdapter(mediumAdapter); mediumRecyclerView.setVisibility(INVISIBLE);
ItemTouchListenerDispatcher dispatcher = new ItemTouchListenerDispatcher(this, galleryGestureDetector, fullScreenGestureDetector); smallRecyclerView.addOnItemTouchListener(onItemTouchListener); mediumRecyclerView.addOnItemTouchListener(onItemTouchListener);
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; } } } …
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; } } } …
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; } } } …
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; } } } …
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); }
@Override public boolean onScaleBegin(@NonNull ScaleGestureDetector detector) { mediumRecyclerView.setVisibility(View.VISIBLE); smallRecyclerView.setVisibility(View.VISIBLE); return true; }
@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; }
@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; }
@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; }
@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; }
@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; } } }
@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; } } }
@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; } } }
@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; } } }
@Override public void onClick(@NonNull final View itemView) { ViewGroupOverlay overlay = fullScreenContainer.getOverlay();
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(); } }
@Override public void onClick(@NonNull final View itemView) { ViewGroupOverlay overlay = fullScreenContainer.getOverlay();
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(); } }
@Override public void onClick(@NonNull final View itemView) { ViewGroupOverlay overlay = fullScreenContainer.getOverlay();
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(); } }
@Override public void onClick(@NonNull final View itemView) { ViewGroupOverlay overlay = fullScreenContainer.getOverlay();
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(); } }
@Override public void onClick(@NonNull final View itemView) { ViewGroupOverlay overlay = fullScreenContainer.getOverlay();
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(); } }
private Runnable setTransitionToRecyclerView() { return new Runnable() { @Override public void run() { fullScreenContainer.setBackgroundColor(BLACK); fullScreenContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) {
fullScreenContainer.setBackgroundColor(TRANSPARENT); itemView.animate().x(originX).y(originY) .scaleY(1).scaleX(1).withEndAction( new Runnable() { @Override public void run() {
fullScreenContainer.setVisibility(View.GONE); } }).start(); } }); } }; }
private Runnable setTransitionToRecyclerView() { return new Runnable() { @Override public void run() { fullScreenContainer.setBackgroundColor(BLACK); fullScreenContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) {
fullScreenContainer.setBackgroundColor(TRANSPARENT); itemView.animate().x(originX).y(originY) .scaleY(1).scaleX(1).withEndAction( new Runnable() { @Override public void run() {
fullScreenContainer.setVisibility(View.GONE); } }).start(); } }); } }; }
private Runnable setTransitionToRecyclerView() { return new Runnable() { @Override public void run() { fullScreenContainer.setBackgroundColor(BLACK); fullScreenContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) {
fullScreenContainer.setBackgroundColor(TRANSPARENT); itemView.animate().x(originX).y(originY) .scaleY(1).scaleX(1).withEndAction( new Runnable() { @Override public void run() {
fullScreenContainer.setVisibility(View.GONE); } }).start(); } }); } }; }
private Runnable setTransitionToRecyclerView() { return new Runnable() { @Override public void run() { fullScreenContainer.setBackgroundColor(BLACK); fullScreenContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) {
fullScreenContainer.setBackgroundColor(TRANSPARENT); itemView.animate().x(originX).y(originY) .scaleY(1).scaleX(1).withEndAction( new Runnable() { @Override public void run() {
fullScreenContainer.setVisibility(View.GONE); } }).start(); } }); } }; }
user touch
make it look cool
parents&paddings
@rallat github.com/rallat/smokeAndMirrors
github.com/nickbutcher/plaid