Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Filthy Rich Android Clients

Romain Guy
November 14, 2013

Filthy Rich Android Clients

Android provides many APIs and capabilities to liven up dull UIs through graphical effects and animations. Come to this session to learn various techniques for making your mobile clients not just rich, but filthy rich.

Romain Guy

November 14, 2013
Tweet

More Decks by Romain Guy

Other Decks in Programming

Transcript

  1. Ultra-graphically rich applications that ooze cool. They suck the user

    in from the outset and hang on to them with a death grip of excitement. 2 Definition: Filthy Rich Clients 2007
  2. Ultra-graphically rich applications that ooze cool. They suck the user

    in from the outset and hang on to them with a death grip of excitement. 2 Definition: Filthy Rich Clients 2007 2013
  3. Ultra-graphically rich applications that ooze cool. They suck the user

    in from the outset and hang on to them with a death grip of excitement. 2 Definition: Filthy Rich Clients Applications that look cool, run smoothly, and interact well with the user. 2007 2013
  4. #DV13 #FilthyRichAndroid Images with rounded corners • Don’t bake the

    shape in your images • Don’t use intermediate layers • Don’t use clipping • Use shaders! 5
  5. #DV13 #FilthyRichAndroid What is a shader? 6 “A set of

    instructions that computes the source color of a pixel being drawn.” – Chet or Romain, just now
  6. #DV13 #FilthyRichAndroid Android shaders • Similar to OpenGL fragment shaders

    • Not programmable • Subclasses of android.graphics.Shader - BitmapShader - ComposeShader - LinearGradient - RadialGradient - SweepGradient 8
  7. #DV13 #FilthyRichAndroid Back to images 10 BitmapShader shader = new

    BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
  8. #DV13 #FilthyRichAndroid Back to images 10 BitmapShader shader = new

    BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); Paint paint = new Paint(); paint.setAntiAlias(true); paint.setShader(shader);
  9. #DV13 #FilthyRichAndroid Back to images 10 BitmapShader shader = new

    BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); Paint paint = new Paint(); paint.setAntiAlias(true); paint.setShader(shader); RectF rect = new RectF(0.0f, 0.0f, width, height); canvas.drawRoundRect(rect, radius, radius, paint);
  10. #DV13 #FilthyRichAndroid Vignette 14 RadialGradient vignette = new RadialGradient( mRect.centerX(),

    mRect.centerY(), radius, new int[] { 0, 0, 0x7f000000 }, new float[] { 0.0f, 0.7f, 1.0f }, Shader.TileMode.CLAMP);
  11. #DV13 #FilthyRichAndroid Vignette 14 RadialGradient vignette = new RadialGradient( mRect.centerX(),

    mRect.centerY(), radius, new int[] { 0, 0, 0x7f000000 }, new float[] { 0.0f, 0.7f, 1.0f }, Shader.TileMode.CLAMP); Matrix oval = new Matrix(); oval.setScale(1.0f, 0.7f); vignette.setLocalMatrix(oval);
  12. #DV13 #FilthyRichAndroid Vignette 14 RadialGradient vignette = new RadialGradient( mRect.centerX(),

    mRect.centerY(), radius, new int[] { 0, 0, 0x7f000000 }, new float[] { 0.0f, 0.7f, 1.0f }, Shader.TileMode.CLAMP); Matrix oval = new Matrix(); oval.setScale(1.0f, 0.7f); vignette.setLocalMatrix(oval); mPaint.setShader(new ComposeShader( mBitmapShader, vignette, PorterDuff.Mode.SRC_OVER));
  13. #DV13 #FilthyRichAndroid Animation APIs • View properties: ViewPropertyAnimator • Everything

    else: ObjectAnimator 17 view.animate().alpha(0).translationX(-500); ObjectAnimator.ofFloat(view, "someProperty", 0).start();
  14. #DV13 #FilthyRichAndroid ListView Animations • Recycling containers are tricky -

    Views != items • Avoid per-frame layout • Determine before/after - animate those changes 20
  15. #DV13 #FilthyRichAndroid Shadowed Background 21 protected void onDraw(Canvas canvas) {

    if (mShowing) { if (mUpdateBounds) { mShadowedBackground.setBounds(0, 0, getWidth(), mOpenAreaHeight); } canvas.save(); canvas.translate(0, mOpenAreaTop); mShadowedBackground.draw(canvas); canvas.restore(); } }
  16. #DV13 #FilthyRichAndroid Adapters and Stable IDs 22 public class StableArrayAdapter

    extends ArrayAdapter<String> { // ... other methods... @Override public long getItemId(int position) { String item = getItem(position); return mIdMap.get(item); } @Override public boolean hasStableIds() { return true; } }
  17. #DV13 #FilthyRichAndroid Swiping: Move/Fade 23 public boolean onTouch(final View v,

    MotionEvent event) { switch (event.getAction()) { // skipping DOWN/CANCEL/UP events case MotionEvent.ACTION_MOVE: { if (!mSwiping) { if (deltaXAbs > mSwipeSlop) { mSwiping = true; mListView.requestDisallowInterceptTouchEvent(true); mBackgroundContainer.showBackground(v.getTop(), v.getHeight()); } } if (mSwiping) { v.setTranslationX((x - mDownX)); v.setAlpha(1 - deltaXAbs / v.getWidth()); } } break; } }
  18. #DV13 #FilthyRichAndroid Animate out 24 v.animate().setDuration(duration). alpha(endAlpha).translationX(endX). withEndAction(new Runnable() {

    @Override public void run() { v.setAlpha(1); v.setTranslationX(0); animateRemoval(mListView, v); } });
  19. #DV13 #FilthyRichAndroid Animate closing the gap 25 private void animateRemoval(final

    ListView listview, View viewToRemove) { // [ Get startTop for all views ] // Delete the item from the adapter int position = mListView.getPositionForView(viewToRemove); mAdapter.remove(mAdapter.getItem(position)); final ViewTreeObserver observer = listview.getViewTreeObserver(); observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { public boolean onPreDraw() { observer.removeOnPreDrawListener(this); for (int i = 0; i < listview.getChildCount(); ++i) { // [ get current view top ] child.setTranslationY(startTop - top); child.animate().setDuration(MOVE_DURATION).translationY(0); child.animate().withEndAction(new Runnable() { public void run() { mBackgroundContainer.hideBackground(); mSwiping = false; mListView.setEnabled(true); } }); } return true; } }); }
  20. #DV13 #FilthyRichAndroid Circular reveal • Technique similar to images with

    rounded corners - Uses a BitmapShader • The mask is not a vector shape • Uses an ALPHA_8 bitmap as the mask - Converted from any type of bitmap 28
  21. #DV13 #FilthyRichAndroid Capturing the content 30 private static Bitmap createBitmap(View

    target) { Bitmap b = Bitmap.createBitmap( target.getWidth(), target.getHeight(), Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b); target.draw(c); return b; }
  22. #DV13 #FilthyRichAndroid Loading the alpha mask 31 private Bitmap loadAsAlphaMask(int

    maskId) { // Attempt to load the bitmap as an alpha mask BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inPreferredConfig = Bitmap.Config.ALPHA_8; Bitmap b = BitmapFactory.decodeResource( mRes, maskId, opts); // If it failed, extract the alpha if (b.getConfig() == Bitmap.Config.ALPHA_8) { return b; } else { return b.extractAlpha(); } }
  23. #DV13 #FilthyRichAndroid Setting up the shader 32 private void createShader()

    { View target = getRootView().findViewById(mTargetId); mTargetBitmap = createBitmap(target); Shader targetShader = new BitmapShader(mTargetBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); mPaint.setShader(targetShader); }
  24. #DV13 #FilthyRichAndroid Drawing the spotlight 33 protected void onDraw(Canvas canvas)

    { mMatrix.setScale( 1.0f / mMaskScale, 1.0f / mMaskScale); mMatrix.preTranslate(-getMaskX(), -getMaskY()); mPaint.getShader().setLocalMatrix(mMatrix); canvas.translate(getMaskX(), getMaskY()); canvas.scale(mMaskScale, mMaskScale); canvas.drawBitmap(mMask, 0.0f, 0.0f, mPaint); }
  25. #DV13 #FilthyRichAndroid Setting up the animations 35 moveLeft = ObjectAnimator.ofFloat(spot,

    "maskX", leftPos); scaleUp = ObjectAnimator.ofFloat(spot, "maskScale", scale1); moveCenter = ObjectAnimator.ofFloat(spot, "maskX", centerX); moveUp = ObjectAnimator.ofFloat(spot, "maskY", centerY); scaleUp2 = ObjectAnimator.ofFloat(spot, "maskScale", scale2);
  26. #DV13 #FilthyRichAndroid Choreographing 36 AnimatorSet set = new AnimatorSet(); set.play(moveLeft).with(scaleUp);

    set.play(moveCenter).after(scaleUp); set.play(moveUp).after(scaleUp); set.play(scaleUp2).after(scaleUp); set.start();
  27. #DV13 #FilthyRichAndroid Filter reveal • Same exact implementation as before

    • Draws the “spotlight“ on top of original photo • Spot’s position depends on where you tapped the button 38
  28. #DV13 #FilthyRichAndroid Path clipping 41 Path clip = new Path();

    clip.addCircle(x, y, radius, Path.Direction.CW); canvas.clipPath(clip); drawContent();
  29. #DV13 #FilthyRichAndroid Path clipping • Pros - Easy to implement

    - Uses less memory - Faster to setup (no Bitmap copy) • Cons - Android 4.3+ only with hardware acceleration - No antialiasing - Can be very expensive - Increases overdraw 42
  30. #DV13 #FilthyRichAndroid Custom Activity Transitions • Standard window animations -

    default: scale/fade - customize: slide, fade, scale - Also thumbnail scale/crossfade • ... But that’s it • Totally custom requires in-activity animations 44
  31. #DV13 #FilthyRichAndroid Custom Activity Transitions • Disable window animations •

    Animate exiting activity • Launch new activity with transparent window • Animate content when activity comes up 45
  32. #DV13 #FilthyRichAndroid Grayscale thumbnails 47 ColorMatrix grayMatrix = new ColorMatrix();

    grayMatrix.setSaturation(0); ColorMatrixColorFilter grayscaleFilter = new ColorMatrixColorFilter(grayMatrix); thumbnailDrawable.setColorFilter(grayscaleFilter);
  33. #DV13 #FilthyRichAndroid Drop shadow container 48 protected void onDraw(Canvas canvas)

    { for (int i = 0; i < getChildCount(); ++i) { View child = getChildAt(i); if (child.getVisibility() != View.VISIBLE || child.getAlpha() == 0) { continue; } int depthFactor = (int) (80 * mShadowDepth); canvas.save(); canvas.translate(child.getLeft() + depthFactor, child.getTop() + depthFactor); canvas.concat(child.getMatrix()); tempShadowRectF.right = child.getWidth(); tempShadowRectF.bottom = child.getHeight(); canvas.drawBitmap(mShadowBitmap, sShadowRect, tempShadowRectF, mShadowPaint); canvas.restore(); } }
  34. #DV13 #FilthyRichAndroid Drop shadow depth 49 public void setShadowDepth(float depth)

    { if (depth != mShadowDepth) { mShadowDepth = depth; mShadowPaint.setAlpha( (int) (100 + 150 * (1 - mShadowDepth))); invalidate(); } }
  35. #DV13 #FilthyRichAndroid Transparent activity background 50 <activity ... android:theme="@style/Transparent" >

    </activity> <style name="Transparent"> <item name="android:windowNoTitle">true</item> <item name="android:windowIsTranslucent">true</item> <item name="android:windowBackground">@android:color/transparent</item> </style>
  36. #DV13 #FilthyRichAndroid Launch sub-activity 51 int[] screenLocation = new int[2];

    v.getLocationOnScreen(screenLocation); PictureData info = mPicturesData.get(v); int orientation = getResources().getConfiguration().orientation; Intent subActivity = new Intent(ActivityAnimations.this, PictureDetailsActivity.class); subActivity.putExtra(PACKAGE + ".orientation", orientation). putExtra(PACKAGE + ".resourceId", info.resourceId). putExtra(PACKAGE + ".left", screenLocation[0]). putExtra(PACKAGE + ".top", screenLocation[1]). putExtra(PACKAGE + ".width", v.getWidth()). putExtra(PACKAGE + ".height", v.getHeight()). putExtra(PACKAGE + ".description", info.description); startActivity(subActivity); overridePendingTransition(0, 0);
  37. #DV13 #FilthyRichAndroid Get animation start values 52 Bundle bundle =

    getIntent().getExtras(); Bitmap bitmap = BitmapUtils.getBitmap(getResources(), bundle.getInt(PACKAGE_NAME + ".resourceId")); String description = bundle.getString(PACKAGE_NAME + ".description"); final int thumbnailTop = bundle.getInt(PACKAGE_NAME + ".top"); final int thumbnailLeft = bundle.getInt(PACKAGE_NAME + ".left"); final int thumbnailWidth = bundle.getInt(PACKAGE_NAME + ".width"); final int thumbnailHeight = bundle.getInt(PACKAGE_NAME + ".height"); mOriginalOrientation = bundle.getInt(PACKAGE_NAME + ".orientation");
  38. #DV13 #FilthyRichAndroid Get animation end values 53 ViewTreeObserver observer =

    mImageView.getViewTreeObserver(); observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { mImageView.getViewTreeObserver().removeOnPreDrawListener(this); int[] screenLocation = new int[2]; mImageView.getLocationOnScreen(screenLocation); mLeftDelta = thumbnailLeft - screenLocation[0]; mTopDelta = thumbnailTop - screenLocation[1]; mWidthScale = (float) thumbnailWidth / mImageView.getWidth(); mHeightScale = (float) thumbnailHeight / mImageView.getHeight(); runEnterAnimation(); return true; } });
  39. #DV13 #FilthyRichAndroid Animate thumbnail & description 54 mImageView.setPivotX(0); mImageView.setPivotY(0); mImageView.setScaleX(mWidthScale);

    mImageView.setScaleY(mHeightScale); mImageView.setTranslationX(mLeftDelta); mImageView.setTranslationY(mTopDelta); mTextView.setAlpha(0); mImageView.animate().setDuration(duration). scaleX(1).scaleY(1). translationX(0).translationY(0). setInterpolator(sDecelerator). withEndAction(new Runnable() { public void run() { mTextView.setTranslationY(-mTextView.getHeight()); mTextView.animate().setDuration(duration/2). translationY(0).alpha(1). setInterpolator(sDecelerator); } });
  40. #DV13 #FilthyRichAndroid Colorize thumbnail 56 ObjectAnimator colorizer = ObjectAnimator.ofFloat( PictureDetailsActivity.this,

    "saturation", 0, 1); colorizer.start(); public void setSaturation(float value) { colorizerMatrix.setSaturation(value); ColorMatrixColorFilter colorizerFilter = new ColorMatrixColorFilter(colorizerMatrix); mBitmapDrawable.setColorFilter(colorizerFilter); }
  41. #DV13 #FilthyRichAndroid Animate back to main activity 58 @Override public

    void onBackPressed() { runExitAnimation(new Runnable() { public void run() { finish(); } }); } @Override public void finish() { super.finish(); overridePendingTransition(0, 0); }
  42. #DV13 #FilthyRichAndroid 63 for (int x = 0; x <

    mNumberOfFolds; x++) { src = mFoldRectArray[x]; canvas.save(); canvas.concat(mMatrix[x]); canvas.clipRect(0, 0, src.width(), src.height()); canvas.translate(-src.left, 0); super.dispatchDraw(canvas); if (x % 2 == 0) { canvas.drawRect(0, 0, mFoldW, mFoldH, mSolidShadow); } else { canvas.drawRect(0, 0, mFoldW, mFoldH, mSoftShadow); } canvas.restore(); }
  43. #DV13 #FilthyRichAndroid 63 for (int x = 0; x <

    mNumberOfFolds; x++) { src = mFoldRectArray[x]; canvas.save(); canvas.concat(mMatrix[x]); canvas.clipRect(0, 0, src.width(), src.height()); canvas.translate(-src.left, 0); super.dispatchDraw(canvas); if (x % 2 == 0) { canvas.drawRect(0, 0, mFoldW, mFoldH, mSolidShadow); } else { canvas.drawRect(0, 0, mFoldW, mFoldH, mSoftShadow); } canvas.restore(); }
  44. #DV13 #FilthyRichAndroid 63 for (int x = 0; x <

    mNumberOfFolds; x++) { src = mFoldRectArray[x]; canvas.save(); canvas.concat(mMatrix[x]); canvas.clipRect(0, 0, src.width(), src.height()); canvas.translate(-src.left, 0); super.dispatchDraw(canvas); if (x % 2 == 0) { canvas.drawRect(0, 0, mFoldW, mFoldH, mSolidShadow); } else { canvas.drawRect(0, 0, mFoldW, mFoldH, mSoftShadow); } canvas.restore(); }
  45. #DV13 #FilthyRichAndroid 63 for (int x = 0; x <

    mNumberOfFolds; x++) { src = mFoldRectArray[x]; canvas.save(); canvas.concat(mMatrix[x]); canvas.clipRect(0, 0, src.width(), src.height()); canvas.translate(-src.left, 0); super.dispatchDraw(canvas); if (x % 2 == 0) { canvas.drawRect(0, 0, mFoldW, mFoldH, mSolidShadow); } else { canvas.drawRect(0, 0, mFoldW, mFoldH, mSoftShadow); } canvas.restore(); }
  46. #DV13 #FilthyRichAndroid Color Filters • Can be used to modify

    a shader • Subclasses of ColorFilter - ColorMatrixColorFilter - LightingColorFilter - PorterDuffColorFilter 64
  47. #DV13 #FilthyRichAndroid Sepia Effect 65 ColorMatrix m1 = new ColorMatrix();

    ColorMatrix m2 = new ColorMatrix(); m1.setSaturation(0.1f); m2.setScale(1f, 0.95f, 0.82f, 1.0f); m1.setConcat(m2, m1); mSepiaPaint.setColorFilter( new ColorMatrixColorFilter(m1));
  48. #DV13 #FilthyRichAndroid Smoother is Better • Consistent frame rate •

    Avoid hiccups • Avoid large steps over few frames 67
  49. #DV13 #FilthyRichAndroid Only Draw What You Need (ODWYN) • Prefer

    invalidate(l, t, r, b) over invalidate() • Only invalidate custom views that actually change • Let the framework invalidate standard views 68
  50. #DV13 #FilthyRichAndroid Avoid Overdraw • Developer options -> Show Overdraw

    • Window background vs. opaque containers vs. opaque views 69
  51. #DV13 #FilthyRichAndroid Get Off that UI Thread! • Avoid expensive

    operations on UI thread - network, database, bitmaps, ... • AsyncTask is your friend 70
  52. #DV13 #FilthyRichAndroid Avoid Garbage Collection • ... especially during animations

    • Lots of small objects will eventually cause GC • Avoid Iterators, temporary objects - Consider cached objects for temporaries • Use Allocation Tracker in DDMS 71
  53. #DV13 #FilthyRichAndroid clipPath • Not always the fastest way to

    clip to a path • Doesn’t support antialiasing • Try BitmapShader 72
  54. #DV13 #FilthyRichAndroid Consider Time Travel • Go see Android Performance

    Workshop 2 days ago - Memory - Performance tips - Tools - Case studies 73
  55. #DV13 #FilthyRichAndroid For More Information • Google I/O talks •

    Parleys.com talks • Devbytes on YouTube 74 Chet graphics-geek.blogspot.com google.com/+ChetHaase @chethaase Romain: curious-creature.org google.com/+RomainGuy @romainguy