Slide 1

Slide 1 text

Filthy Rich Clients Romain Guy Chet Haase google.com/+RomainGuy google.com/+ChetHaase @romainguy @chethaase

Slide 2

Slide 2 text

Filthy Rich Clients Romain Guy Chet Haase google.com/+RomainGuy google.com/+ChetHaase @romainguy @chethaase Android

Slide 3

Slide 3 text

Filthy Rich Clients Romain Guy Chet Haase google.com/+RomainGuy google.com/+ChetHaase @romainguy @chethaase Android Shiny!

Slide 4

Slide 4 text

2 Definition: Filthy Rich Clients

Slide 5

Slide 5 text

2 Definition: Filthy Rich Clients 2007

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Graphics

Slide 10

Slide 10 text

#DV13 #FilthyRichAndroid Images with rounded corners 4

Slide 11

Slide 11 text

#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

Slide 12

Slide 12 text

#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

Slide 13

Slide 13 text

#DV13 #FilthyRichAndroid Example 7 Paint p = new Paint(); p.setColor(Color.RED);

Slide 14

Slide 14 text

#DV13 #FilthyRichAndroid Example 7 Paint p = new Paint(); p.setColor(Color.RED); Simplest shader ever

Slide 15

Slide 15 text

#DV13 #FilthyRichAndroid Android shaders • Similar to OpenGL fragment shaders • Not programmable • Subclasses of android.graphics.Shader - BitmapShader - ComposeShader - LinearGradient - RadialGradient - SweepGradient 8

Slide 16

Slide 16 text

#DV13 #FilthyRichAndroid How drawing works (simplified) 9 + drawRoundRect Paint Mask Shader

Slide 17

Slide 17 text

#DV13 #FilthyRichAndroid Back to images 10

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

#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);

Slide 20

Slide 20 text

#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);

Slide 21

Slide 21 text

#DV13 #FilthyRichAndroid Contrary to the previous slide… Never allocate in draw() methods. 11

Slide 22

Slide 22 text

#DV13 #FilthyRichAndroid 12 Vignette No vignette

Slide 23

Slide 23 text

#DV13 #FilthyRichAndroid ComposeShader 13 LinearGradient BitmapShader ComposeShader xfermode

Slide 24

Slide 24 text

#DV13 #FilthyRichAndroid Vignette 14

Slide 25

Slide 25 text

#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);

Slide 26

Slide 26 text

#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);

Slide 27

Slide 27 text

#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));

Slide 28

Slide 28 text

#DV13 #FilthyRichAndroid Works with any shape 15 drawCircle() drawPath() drawPath()

Slide 29

Slide 29 text

Animation

Slide 30

Slide 30 text

#DV13 #FilthyRichAndroid Animation APIs • View properties: ViewPropertyAnimator • Everything else: ObjectAnimator 17 view.animate().alpha(0).translationX(-500); ObjectAnimator.ofFloat(view, "someProperty", 0).start();

Slide 31

Slide 31 text

#DV13 #FilthyRichAndroid Timing is Everything • Make those animations short! • And non-linear 18

Slide 32

Slide 32 text

ListView Animation!

Slide 33

Slide 33 text

#DV13 #FilthyRichAndroid ListView Animations • Recycling containers are tricky - Views != items • Avoid per-frame layout • Determine before/after - animate those changes 20

Slide 34

Slide 34 text

#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(); } }

Slide 35

Slide 35 text

#DV13 #FilthyRichAndroid Adapters and Stable IDs 22 public class StableArrayAdapter extends ArrayAdapter { // ... other methods... @Override public long getItemId(int position) { String item = getItem(position); return mIdMap.get(item); } @Override public boolean hasStableIds() { return true; } }

Slide 36

Slide 36 text

#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; } }

Slide 37

Slide 37 text

#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); } });

Slide 38

Slide 38 text

#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; } }); }

Slide 39

Slide 39 text

Circular Reveal!

Slide 40

Slide 40 text

#DV13 #FilthyRichAndroid Circular reveal 27

Slide 41

Slide 41 text

#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

Slide 42

Slide 42 text

#DV13 #FilthyRichAndroid Circular reveal 29 ALPHA_8 bitmap mask Bitmap texture

Slide 43

Slide 43 text

#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; }

Slide 44

Slide 44 text

#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(); } }

Slide 45

Slide 45 text

#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); }

Slide 46

Slide 46 text

#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); }

Slide 47

Slide 47 text

#DV13 #FilthyRichAndroid Animating the spotlight 34

Slide 48

Slide 48 text

#DV13 #FilthyRichAndroid Animating the spotlight 34 Move left & scale up

Slide 49

Slide 49 text

#DV13 #FilthyRichAndroid Animating the spotlight 34 Move to center & scale up

Slide 50

Slide 50 text

#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);

Slide 51

Slide 51 text

#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();

Slide 52

Slide 52 text

#DV13 #FilthyRichAndroid Android 4.4 Photo Editor 37

Slide 53

Slide 53 text

#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

Slide 54

Slide 54 text

#DV13 #FilthyRichAndroid Google Now 39

Slide 55

Slide 55 text

#DV13 #FilthyRichAndroid 40

Slide 56

Slide 56 text

#DV13 #FilthyRichAndroid 40

Slide 57

Slide 57 text

#DV13 #FilthyRichAndroid 40 No antialiasing!

Slide 58

Slide 58 text

#DV13 #FilthyRichAndroid Path clipping 41 Path clip = new Path(); clip.addCircle(x, y, radius, Path.Direction.CW); canvas.clipPath(clip); drawContent();

Slide 59

Slide 59 text

#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

Slide 60

Slide 60 text

Activity Transitions!

Slide 61

Slide 61 text

#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

Slide 62

Slide 62 text

#DV13 #FilthyRichAndroid Custom Activity Transitions • Disable window animations • Animate exiting activity • Launch new activity with transparent window • Animate content when activity comes up 45

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

#DV13 #FilthyRichAndroid Grayscale thumbnails 47 ColorMatrix grayMatrix = new ColorMatrix(); grayMatrix.setSaturation(0); ColorMatrixColorFilter grayscaleFilter = new ColorMatrixColorFilter(grayMatrix); thumbnailDrawable.setColorFilter(grayscaleFilter);

Slide 65

Slide 65 text

#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(); } }

Slide 66

Slide 66 text

#DV13 #FilthyRichAndroid Drop shadow depth 49 public void setShadowDepth(float depth) { if (depth != mShadowDepth) { mShadowDepth = depth; mShadowPaint.setAlpha( (int) (100 + 150 * (1 - mShadowDepth))); invalidate(); } }

Slide 67

Slide 67 text

#DV13 #FilthyRichAndroid Transparent activity background 50 <item name="android:windowNoTitle">true</item> <item name="android:windowIsTranslucent">true</item> <item name="android:windowBackground">@android:color/transparent</item>

Slide 68

Slide 68 text

#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);

Slide 69

Slide 69 text

#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");

Slide 70

Slide 70 text

#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; } });

Slide 71

Slide 71 text

#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); } });

Slide 72

Slide 72 text

#DV13 #FilthyRichAndroid Fade in black background 55 ObjectAnimator.ofInt(mBackground, "alpha", 0, 255). start();

Slide 73

Slide 73 text

#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); }

Slide 74

Slide 74 text

#DV13 #FilthyRichAndroid Animate drop shadow 57 ObjectAnimator shadowAnim = ObjectAnimator.ofFloat( mShadowLayout, "shadowDepth", 0, 1); shadowAnim.start();

Slide 75

Slide 75 text

#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); }

Slide 76

Slide 76 text

Folding Layout!

Slide 77

Slide 77 text

#DV13 #FilthyRichAndroid Fan Fare 60

Slide 78

Slide 78 text

#DV13 #FilthyRichAndroid 61

Slide 79

Slide 79 text

#DV13 #FilthyRichAndroid 62 Matrix.setPolyToPoly()

Slide 80

Slide 80 text

#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(); }

Slide 81

Slide 81 text

#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(); }

Slide 82

Slide 82 text

#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(); }

Slide 83

Slide 83 text

#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(); }

Slide 84

Slide 84 text

#DV13 #FilthyRichAndroid Color Filters • Can be used to modify a shader • Subclasses of ColorFilter - ColorMatrixColorFilter - LightingColorFilter - PorterDuffColorFilter 64

Slide 85

Slide 85 text

#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));

Slide 86

Slide 86 text

Performance

Slide 87

Slide 87 text

#DV13 #FilthyRichAndroid Smoother is Better • Consistent frame rate • Avoid hiccups • Avoid large steps over few frames 67

Slide 88

Slide 88 text

#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

Slide 89

Slide 89 text

#DV13 #FilthyRichAndroid Avoid Overdraw • Developer options -> Show Overdraw • Window background vs. opaque containers vs. opaque views 69

Slide 90

Slide 90 text

#DV13 #FilthyRichAndroid Get Off that UI Thread! • Avoid expensive operations on UI thread - network, database, bitmaps, ... • AsyncTask is your friend 70

Slide 91

Slide 91 text

#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

Slide 92

Slide 92 text

#DV13 #FilthyRichAndroid clipPath • Not always the fastest way to clip to a path • Doesn’t support antialiasing • Try BitmapShader 72

Slide 93

Slide 93 text

#DV13 #FilthyRichAndroid Consider Time Travel • Go see Android Performance Workshop 2 days ago - Memory - Performance tips - Tools - Case studies 73

Slide 94

Slide 94 text

#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

Slide 95

Slide 95 text

#DV13 #FilthyRichAndroid Q&A 75 Filthy Rich Clients: Developing Animated and Graphical Effects