Share
Embed
Advanced Scrolling Techniques on Android
Scrolling is one of the most important gesture on mobile platforms. Indeed, it easily allows you to access a fairly large amount of content just by swiping your fingers on screen. In order to implement such gestures, the Android SDK comes with a bunch of scrolling containers: View, ListView, RecyclerView, ScrollView, etc. While using these components independently is relatively simple, it starts getting complicated when nesting them… In this session you will get a complete overview of how Android deals with scrolling in general and how to master the scrolling mechanism to create compelling mobile user experience.
Cyril Mottier
October 30, 2015
Transcript
100 slides
-
We've always defined ourselves by the ability to overcome the
impossible. And we count these moments. These moments when we dare to aim higher, to break barriers, to reach for the stars, to make the unknown known. We count these moments as our proudest achievements. But we lost all that. Or perhaps we've just forgotten that we are still pioneers. And we've barely begun. And that our greatest accomplishments cannot be behind us, because our destiny lies above us. - Cooper (Interstellar) Don't talk like one of them. You're not! Even if you'd like to be. To them, you're just a freak, like me! They need you right now, but when they don't, they'll cast you out, like a leper! You see, their morals, their code, it's a bad joke. Dropped at the first sign of trouble. They're only as good as the world allows them to be. I'll show you. When the chips are down, these... these civilized people, they'll eat each other. See, I'm not a monster. -
Optician Joe We've always defined ourselves by the ability to
overcome the impossible. And we count these moments. These moments when we dare to aim higher, to break barriers, to reach for the stars, to make the unknown known. We count these moments as our proudest achievements. But we lost all that. Or perhaps we've just forgotten that we are still pioneers. And we've barely begun. And that our greatest accomplishments cannot be behind us, because our destiny lies above us. - Cooper (Interstellar) Don't talk like one of them. You're not! Even if you'd like to be. To them, you're just a freak, like me! They need you right now, but when they don't, they'll cast you out, like a leper! You see, their morals, their code, it's a bad joke. Dropped at the first sign of trouble. They're only as good as the world allows them to be. I'll show you. When the chips are down, these... these civilized people, they'll eat each other. See, I'm not a monster. I'm just ahead of the curve. - The Joker (The Dark Knight) My name is Max. My world is fire and blood. Once, I was a cop. A road warrior searching for a righteous cause. As the world fell, each of us in our own way was broken. It was hard to know who was more crazy... me... or everyone else. - Max (Mad Max Fury Road) What is the most resilient parasite? Bacteria? A virus? An intestinal worm? An idea. Resilient... highly contagious. Once an idea has taken hold of the brain it's almost impossible to eradicate. An idea that is fully formed - fully understood - that sticks; right in there somewhere. - Cobb (Inception) It's light. Handle's adjustable for easy carrying, good for righties and lefties. Breaks down into four parts, undetectable by x-ray, ideal for quick, discreet interventions. A word on firepower. Titanium recharger, three thousand round clip with bursts of three to three hundred, and with the Replay button - another Zorg invention - it's even easier. - Zorg (Th Fifth Element) Anyway, like I was sayin', shrimp is the fruit of the sea. You can barbecue it, boil it, broil it, bake it, saute it. Dey's uh, shrimp- kabobs, shrimp creole, shrimp gumbo. Pan fried, deep fried, stir-fried. There's pineapple shrimp, lemon shrimp, coconut shrimp, pepper shrimp, shrimp soup, shrimp stew, shrimp salad, shrimp and potatoes, shrimp burger, shrimp sandwich. That- that's about it. - Bubba (Forrest Gump) You tell God the Father it was a kindness you done. I know you hurtin' and worryin', I can feel it on you, but you oughta quit on it now. Because I want it over and done. I do. I'm tired, boss. Tired of bein' on the road, lonely as a sparrow in the rain. Tired of not ever having me a buddy to be with, or tell me where we's coming from or going to, or why. Mostly I'm tired of people being ugly to each other. I'm tired of all the pain I feel and hear in the world everyday. There's too much of it. It's like pieces of glass in my head all the time. Can you understand? - John Coffey (The Green Mile) You're trying? Now? Where were you when it mattered? I needed this guy back when I was a kid. I don't need you now. It's too late now. Everything's already happened. You and Brendan don't seem to understand that. Let me explain something to you: the only thing I have in common with Brendan Conlon is that we have absolutely no use for you. - Tom Colon (Warrior) -
Salesman Max We've always defined ourselves by the ability to
overcome the impossible. And we count these moments. These moments when we dare to aim higher, to break barriers, to reach for the stars, to make the unknown known. We count these moments as our proudest achievements. But we lost all that. Or perhaps we've just forgotten that we are still pioneers. And we've barely begun. And that our greatest accomplishments cannot be behind us, because our destiny lies above us. - Cooper (Interstellar) Don't talk like one of them. You're not! Even if you'd like to be. To them, you're just a freak, like me! They need you right now, but when they don't, they'll cast you out, like a leper! You see, their morals, their code, it's a bad joke. Dropped at the first sign of trouble. They're only as good as the world allows them to be. I'll show you. When the chips are down, these... these civilized people, they'll eat each other. See, I'm not a monster. I'm just ahead of the curve. - The Joker (The Dark Knight) My name is Max. My world is fire and blood. Once, I was a cop. A road warrior searching for a righteous cause. As the world fell, each of us in our own way was broken. It was hard to know who was more crazy... me... or everyone else. - Max (Mad Max Fury Road) What is the most resilient parasite? Bacteria? A virus? An intestinal worm? An idea. Resilient... highly contagious. Once an idea has taken hold of the brain it's almost impossible to eradicate. An idea that is fully formed - fully understood - that sticks; right in there somewhere. - Cobb (Inception) It's light. Handle's adjustable for easy carrying, good for righties and lefties. Breaks down into four parts, undetectable by x-ray, ideal for quick, discreet interventions. A word on firepower. Titanium recharger, three thousand round clip with bursts of three to three hundred, and with the Replay button - another Zorg invention - it's even easier. - Zorg (Th Fifth Element) Anyway, like I was sayin', shrimp is the fruit of the sea. You can barbecue it, boil it, broil it, bake it, saute it. Dey's uh, shrimp-kabobs, shrimp creole, shrimp gumbo. Pan fried, deep fried, stir-fried. There's pineapple shrimp, lemon shrimp, coconut shrimp, pepper shrimp, shrimp soup, shrimp stew, shrimp salad, shrimp and potatoes, -
We've always defined ourselves by the ability to overcome the
impossible. And we count these moments. These moments when we dare to aim higher, to break barriers, to reach for the stars, to make the unknown known. We count these moments as our proudest achievements. But we lost all that. Or perhaps we've just forgotten that we are still pioneers. And we've barely begun. And that our greatest accomplishments cannot be behind us, because our destiny lies above us. - Cooper (Interstellar) Don't talk like one of them. You're not! Even if you'd like to be. To them, you're just a freak, like me! They need you right now, but when they don't, they'll cast you out, like a leper! You see, their morals, their code, it's a bad joke. Dropped at the first sign of trouble. They're only as good as the world allows them to be. I'll show you. When the chips are down, these... these civilized people, they'll eat each other. See, I'm not a monster. Dev / designer Kevin -
accomplishments cannot be behind us, because our destiny lies above
us. - Cooper (Interstellar) Don't talk like one of them. You're not! Even if you'd like to be. To them, you're just a freak, like me! They need you right now, but when they don't, they'll cast you out, like a leper! You see, their morals, their code, it's a bad joke. Dropped at the first sign of trouble. They're only as good as the world allows them to be. I'll show you. When the chips are down, these... these civilized people, they'll eat each other. See, I'm not a monster. I'm just ahead of the curve. - The Joker (The Dark Knight) My name is Max. My world is fire and blood. Once, I was a cop. A road warrior searching for a righteous cause. As the world fell, each of us in our own way was broken. It was hard to know who was more crazy... me... or everyone else. - Max (Mad Max Fury Road) What is the most resilient parasite? Bacteria? A virus? Dev / designer Kevin -
Scroll /skɹoʊl/ The general process of moving the viewport -
that is, the ‘window’ of content you’re looking at. -
Drag /drӕɡ/ A type of scrolling that occurs when a
user drags her finger across the touch screen. -
Fling /flɪŋ/ A type of scrolling that occurs when a
user drags and lifts her finger quickly. It is also known as ‘inertia scrolling’. -
View TextView AbsListView ScrollView ListView Horizontal ScrollView GridView ViewPager RecyclerView
MapView Android ViewGroup -
public class YoScrollView extends ScrollView { @Override protected void
onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); // Called when scroll properties changed } // ... } At the View level -
public class YoScrollView extends ScrollView { @Override protected void
onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); // Called when scroll properties changed } // ... } At the View level Old & new scroll values available -
findViewById(android.R.id.scrollView) .setOnScrollChangeListener(new OnScrollChangeListener() { @Override public void onScrollChange(View v, int
scrollX, int scrollY, int oldScrollX, int oldScrollY) { // Called when scroll properties changed } }); At the View level Requires API 23 -
findViewById(android.R.id.scrollView) .setOnScrollChangeListener(new OnScrollChangeListener() { @Override public void onScrollChange(View v, int
scrollX, int scrollY, int oldScrollX, int oldScrollY) { // Called when scroll properties changed } }); At the View level Requires API 23 No more @Overrides and custom Views -
getWindow().getDecorView() .getViewTreeObserver() .addOnScrollChangedListener( new ViewTreeObserver.OnScrollChangedListener() { @Override public void onScrollChanged()
{ // A View in the hierarchy scrolled } }); At the View hierarchy level -
getWindow().getDecorView() .getViewTreeObserver() .addOnScrollChangedListener( new ViewTreeObserver.OnScrollChangedListener() { @Override public void onScrollChanged()
{ // A View in the hierarchy scrolled } }); At the View hierarchy level No parameters, no info : I’m blind! -
aView.setOnScrollChangeListener(new OnScrollChangeListener() { @Override public void onScrollChange(View v, int scrollX,
int scrollY, int oldScrollX, int oldScrollY) { Log.d(TAG, "(" + scrollX + ", " + scrollY + ")"); } }); -
aView instanceof ScrollView scroll(0, 1) scroll(0, 12) scroll(0, 23) scroll(0,
46) scroll(0, 67) scroll(0, 69) scroll(0, 70) _ -
aView instanceof WebView scroll(0, 1) scroll(0, 9) scroll(0, 12) scroll(0,
22) scroll(0, 28) scroll(0, 34) scroll(0, 36) _ -
aView instanceof ListView scroll(0, 0) scroll(0, 0) scroll(0, 0) scroll(0,
0) scroll(0, -5) scroll(0, -1) scroll(0, 0) _ -
Initialisation setScrollContainer(true) Inform the system the View is a scrollable
container in its Window -
Initialisation @Override public boolean shouldDelayChildPressedState() { return true; } Prevents
the pressed state from appearing when the user is actually trying to scroll the content. -
Movement tracking @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction())
{ case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; case MotionEvent.ACTION_CANCEL: break; } return true; } -
Movement tracking @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction())
{ case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; case MotionEvent.ACTION_CANCEL: break; } return true; } Stop animations and start a new drag gesture -
Movement tracking @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction())
{ case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; case MotionEvent.ACTION_CANCEL: break; } return true; } Move/drag the object according to the pointer -
Movement tracking @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction())
{ case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; case MotionEvent.ACTION_CANCEL: break; } return true; } Compute the object’s velocity and start animating it -
Movement tracking @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction())
{ case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; case MotionEvent.ACTION_CANCEL: break; } return true; } The gesture was intercepted by a parent -
Inertia scrolling @Override public boolean onTouchEvent(MotionEvent event) { if (mVelocityTracker
== null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (event.getAction()) { case MotionEvent.ACTION_UP: mVelocityTracker.computeCurrentVelocity(1000); if (mVelocityTracker.getXVelocity() > mMinVelocity) { // Animate the object } // no break; case MotionEvent.ACTION_CANCEL: mVelocityTracker.recycle(); mVelocityTracker = null; break; } return true; } -
Inertia scrolling @Override public boolean onTouchEvent(MotionEvent event) { if (mVelocityTracker
== null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (event.getAction()) { case MotionEvent.ACTION_UP: mVelocityTracker.computeCurrentVelocity(1000); if (mVelocityTracker.getXVelocity() > mMinVelocity) { // Animate the object } // no break; case MotionEvent.ACTION_CANCEL: mVelocityTracker.recycle(); mVelocityTracker = null; break; } return true; } Add MotionEvents from the current gesture to a VelocityTracker -
Inertia scrolling @Override public boolean onTouchEvent(MotionEvent event) { if (mVelocityTracker
== null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (event.getAction()) { case MotionEvent.ACTION_UP: mVelocityTracker.computeCurrentVelocity(1000); if (mVelocityTracker.getXVelocity() > mMinVelocity) { // Animate the object } // no break; case MotionEvent.ACTION_CANCEL: mVelocityTracker.recycle(); mVelocityTracker = null; break; } return true; } Finger velocity was important enough to initiate a fling -
Inertia scrolling @Override public boolean onTouchEvent(MotionEvent event) { if (mVelocityTracker
== null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (event.getAction()) { case MotionEvent.ACTION_UP: mVelocityTracker.computeCurrentVelocity(1000); if (mVelocityTracker.getXVelocity() > mMinVelocity) { // Animate the object } // no break; case MotionEvent.ACTION_CANCEL: mVelocityTracker.recycle(); mVelocityTracker = null; break; } return true; } Clean up our mess with the VelocityTracker -
Inertia scrolling public void fling() { mOverScroller.fling(mStartX, mStartY, // start
x/y mVX, mVY, // velocity x/y mMinX, mMaxX, // min/max x mMinY, mMaxY); // min/max y postOnAnimation(mScrollRunnable); } private final Runnable mScrollRunnable = new Runnable() { @Override public void run() { if (mOverScroller.computeScrollOffset()) { final int x = mOverScroller.getCurrX(); final int y = mOverScroller.getCurrY(); // Move object to (x, y). postOnAnimation(this); } else { // animation is over } } }; -
Inertia scrolling public void fling() { mOverScroller.fling(mStartX, mStartY, // start
x/y mVX, mVY, // velocity x/y mMinX, mMaxX, // min/max x mMinY, mMaxY); // min/max y postOnAnimation(mScrollRunnable); } private final Runnable mScrollRunnable = new Runnable() { @Override public void run() { if (mOverScroller.computeScrollOffset()) { final int x = mOverScroller.getCurrX(); final int y = mOverScroller.getCurrY(); // Move object to (x, y). postOnAnimation(this); } else { // animation is over } } }; Pass all gesture info to initiate a fling animation -
Inertia scrolling public void fling() { mOverScroller.fling(mStartX, mStartY, // start
x/y mVX, mVY, // velocity x/y mMinX, mMaxX, // min/max x mMinY, mMaxY); // min/max y postOnAnimation(mScrollRunnable); } private final Runnable mScrollRunnable = new Runnable() { @Override public void run() { if (mOverScroller.computeScrollOffset()) { final int x = mOverScroller.getCurrX(); final int y = mOverScroller.getCurrY(); // Move object to (x, y). postOnAnimation(this); } else { // animation is over } } }; Schedule the next scrolling frame -
Inertia scrolling public void fling() { mOverScroller.fling(mStartX, mStartY, // start
x/y mVX, mVY, // velocity x/y mMinX, mMaxX, // min/max x mMinY, mMaxY); // min/max y postOnAnimation(mScrollRunnable); } private final Runnable mScrollRunnable = new Runnable() { @Override public void run() { if (mOverScroller.computeScrollOffset()) { final int x = mOverScroller.getCurrX(); final int y = mOverScroller.getCurrY(); // Move object to (x, y). postOnAnimation(this); } else { // animation is over } } }; Use the current OverScroller state to determine the object’s position -
Inertia scrolling public void fling() { mOverScroller.fling(mStartX, mStartY, // start
x/y mVX, mVY, // velocity x/y mMinX, mMaxX, // min/max x mMinY, mMaxY); // min/max y postOnAnimation(mScrollRunnable); } private final Runnable mScrollRunnable = new Runnable() { @Override public void run() { if (mOverScroller.computeScrollOffset()) { final int x = mOverScroller.getCurrX(); final int y = mOverScroller.getCurrY(); // Move object to (x, y). postOnAnimation(this); } else { // animation is over } } }; Schedule the next scrolling frame -
Inertia scrolling public void fling() { mOverScroller.fling(mStartX, mStartY, // start
x/y mVX, mVY, // velocity x/y mMinX, mMaxX, // min/max x mMinY, mMaxY); // min/max y postOnAnimation(mScrollRunnable); } private final Runnable mScrollRunnable = new Runnable() { @Override public void run() { if (mOverScroller.computeScrollOffset()) { final int x = mOverScroller.getCurrX(); final int y = mOverScroller.getCurrY(); // Move object to (x, y). postOnAnimation(this); } else { // animation is over } } }; Animation is complete. Let’s rest -
Scrollbars drawing @Override protected int computeVerticalScrollExtent() { return getHeight(); }
@Override protected int computeVerticalScrollOffset() { return mOffsetY; } @Override protected int computeVerticalScrollRange() { return mContentHeight; } -
Scrollbars drawing @Override protected int computeVerticalScrollExtent() { return getHeight(); }
@Override protected int computeVerticalScrollOffset() { return mOffsetY; } @Override protected int computeVerticalScrollRange() { return mContentHeight; } -
Scrollbars drawing @Override protected int computeVerticalScrollExtent() { return getHeight(); }
@Override protected int computeVerticalScrollOffset() { return mOffsetY; } @Override protected int computeVerticalScrollRange() { return mContentHeight; } -
Scrollbars drawing @Override protected int computeVerticalScrollExtent() { return getHeight(); }
@Override protected int computeVerticalScrollOffset() { return mOffsetY; } @Override protected int computeVerticalScrollRange() { return mContentHeight; } -
Edges feedback @Override public void draw(Canvas canvas) { super.draw(canvas); if
(!mTopEdgeEffect.isFinished()) { canvas.save(); canvas.translate(getPaddingLeft(), 0); final int w = getWidth() - getPaddingLeft() - getPaddingRight(); mTopEdgeEffect.setSize(w, getHeight()); if (mTopEdgeEffect.draw(canvas)) { postInvalidateOnAnimation(); } canvas.restore(); } } -
Edges feedback @Override public void draw(Canvas canvas) { super.draw(canvas); if
(!mTopEdgeEffect.isFinished()) { canvas.save(); canvas.translate(getPaddingLeft(), getPaddingTop()); final int w = getWidth() - getPaddingLeft() - getPaddingRight(); mTopEdgeEffect.setSize(w, getHeight()); if (mTopEdgeEffect.draw(canvas)) { postInvalidateOnAnimation(); } canvas.restore(); } } Draw only if animation is in progress -
Edges feedback @Override public void draw(Canvas canvas) { super.draw(canvas); if
(!mTopEdgeEffect.isFinished()) { canvas.save(); canvas.translate(getPaddingLeft(), getPaddingTop()); final int w = getWidth() - getPaddingLeft() - getPaddingRight(); mTopEdgeEffect.setSize(w, getHeight()); if (mTopEdgeEffect.draw(canvas)) { postInvalidateOnAnimation(); } canvas.restore(); } } Move the Canvas to the correct position -
Edges feedback @Override public void draw(Canvas canvas) { super.draw(canvas); if
(!mTopEdgeEffect.isFinished()) { canvas.save(); canvas.translate(getPaddingLeft(), getPaddingTop()); final int w = getWidth() - getPaddingLeft() - getPaddingRight(); mTopEdgeEffect.setSize(w, getHeight()); if (mTopEdgeEffect.draw(canvas)) { postInvalidateOnAnimation(); } canvas.restore(); } } Set the size to the edge feedback -
Edges feedback @Override public void draw(Canvas canvas) { super.draw(canvas); if
(!mTopEdgeEffect.isFinished()) { canvas.save(); canvas.translate(getPaddingLeft(), getPaddingTop()); final int w = getWidth() - getPaddingLeft() - getPaddingRight(); mTopEdgeEffect.setSize(w, getHeight()); if (mTopEdgeEffect.draw(canvas)) { postInvalidateOnAnimation(); } canvas.restore(); } } Draw this frame & schedule the next one -
Edges feedback onAbsorb(int velocity) onPull(float deltaDistance) onRelease() Call when a
fling reaches a scroll boundary -
Edges feedback onAbsorb(int velocity) onPull(float deltaDistance) onRelease() Called when content
is pulled away from an edge -
Nested scrolling ScrollView API 7 API 4 API 21 NestedScrollView
RecyclerView HorizontalScrollView ListView -
Fonts Geared Slab Mission Script Menlo Resources Dressed for Iceland
- Cécile Bernard Moelwynion, Eryri, Cymru - Marc Poppleton Scroll icon - Darren Wilson