You are on page 1of 96

Thursday, November 14, 13

Filthy Rich Clients

Romain Guy Chet Haase

google.com/+RomainGuy google.com/+ChetHaase

@romainguy @chethaase

Thursday, November 14, 13

d i o r d An Filthy Rich Clients

Romain Guy Chet Haase

google.com/+RomainGuy google.com/+ChetHaase

@romainguy @chethaase

Thursday, November 14, 13

d i o r d An Filthy Rich Clients

Shi ny !
@romainguy @chethaase

Romain Guy Chet Haase

google.com/+RomainGuy google.com/+ChetHaase

Thursday, November 14, 13

De!nition: Filthy Rich Clients

2 Thursday, November 14, 13

De!nition: Filthy Rich Clients


2007

2 Thursday, November 14, 13

De!nition: Filthy Rich Clients


2007 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 Thursday, November 14, 13

De!nition: Filthy Rich Clients


2007 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. 2013

2 Thursday, November 14, 13

De!nition: Filthy Rich Clients


2007 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. 2013 Applications that look cool, run smoothly, and interact well with the user.
2 Thursday, November 14, 13

Graphics

Thursday, November 14, 13

Images with rounded corners

#DV13 #FilthyRichAndroid4
Thursday, November 14, 13

Images with rounded corners


Dont bake the shape in your images Dont use intermediate layers Dont use clipping Use shaders!

#DV13 #FilthyRichAndroid5
Thursday, November 14, 13

What is a shader?

A set of instructions that computes the source color of a pixel being drawn. Chet or Romain, just now

#DV13 #FilthyRichAndroid6
Thursday, November 14, 13

Example

Paint p = new Paint(); p.setColor(Color.RED);

#DV13 #FilthyRichAndroid7
Thursday, November 14, 13

Example

Paint p = new Paint(); p.setColor(Color.RED);

t s e l p Sim

r e v e r e d a sh

#DV13 #FilthyRichAndroid7
Thursday, November 14, 13

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

#DV13 #FilthyRichAndroid8
Thursday, November 14, 13

How drawing works (simplified)


Shader Paint + drawRoundRect Mask

#DV13 #FilthyRichAndroid9
Thursday, November 14, 13

Back to images

10 #DV13 #FilthyRichAndroid Thursday, November 14, 13

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

10 #DV13 #FilthyRichAndroid Thursday, November 14, 13

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

10 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Back to images
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 Thursday, November 14, 13

Contrary to the previous slide Never allocate in draw() methods.

11 #DV13 #FilthyRichAndroid Thursday, November 14, 13

e t t igne

No v ig ne t te

12 #DV13 #FilthyRichAndroid Thursday, November 14, 13

ComposeShader

LinearGradient

xfermode

BitmapShader

ComposeShader

13 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Vignette

14 #DV13 #FilthyRichAndroid Thursday, November 14, 13

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

14 #DV13 #FilthyRichAndroid Thursday, November 14, 13

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

14 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Vignette
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));
14 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Works with any shape

drawCircle()

drawPath()

drawPath()
15 #DV13 #FilthyRichAndroid

Thursday, November 14, 13

Animation

Thursday, November 14, 13

Animation APIs
View properties: ViewPropertyAnimator
view.animate().alpha(0).translationX(-500);

Everything else: ObjectAnimator


ObjectAnimator.ofFloat(view, "someProperty", 0).start();

17 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Timing is Everything
Make those animations short! And non-linear

18 #DV13 #FilthyRichAndroid Thursday, November 14, 13

L i s tV

ie w A

nim a

t io n!

Thursday, November 14, 13

ListView Animations
Recycling containers are tricky
- Views != items

Avoid per-frame layout Determine before/after


- animate those changes

20 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Shadowed Background
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(); } }
21 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Adapters and Stable IDs


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; } }
22 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Swiping: Move/Fade
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; } }
23 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Animate out
v.animate().setDuration(duration). alpha(endAlpha).translationX(endX). withEndAction(new Runnable() { @Override public void run() { v.setAlpha(1); v.setTranslationX(0); animateRemoval(mListView, v); } });

24 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Animate closing the gap


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; } }); }
Thursday, November 14, 13 25 #DV13 #FilthyRichAndroid

C i rc u

lar R

e ve a

l!

Thursday, November 14, 13

Circular reveal

27 #DV13 #FilthyRichAndroid Thursday, November 14, 13

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 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Circular reveal
Bitmap texture

ALPHA_8 bitmap mask

29 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Capturing the content

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

30 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Loading the alpha mask


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(); } }
31 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Setting up the shader

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

32 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Drawing the spotlight


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

33 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Animating the spotlight

34 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Animating the spotlight

Move left & scale up


34 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Animating the spotlight

Move to center & scale up


34 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Setting up the animations

moveLeft scaleUp moveCenter moveUp scaleUp2

= = = = =

ObjectAnimator.ofFloat(spot, ObjectAnimator.ofFloat(spot, ObjectAnimator.ofFloat(spot, ObjectAnimator.ofFloat(spot, ObjectAnimator.ofFloat(spot,

"maskX", leftPos); "maskScale", scale1); "maskX", centerX); "maskY", centerY); "maskScale", scale2);

35 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Choreographing

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

36 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Android 4.4 Photo Editor

37 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Filter reveal
Same exact implementation as before Draws the spotlight on top of original photo Spots position depends on where you tapped the button

38 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Google Now

39 #DV13 #FilthyRichAndroid Thursday, November 14, 13

40 #DV13 #FilthyRichAndroid Thursday, November 14, 13

40 #DV13 #FilthyRichAndroid Thursday, November 14, 13

No a n t i a l i a
Thursday, November 14, 13

s i ng!

40 #DV13 #FilthyRichAndroid

Path clipping

Path clip = new Path(); clip.addCircle(x, y, radius, Path.Direction.CW); canvas.clipPath(clip); drawContent();

41 #DV13 #FilthyRichAndroid Thursday, November 14, 13

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 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Ac t i v

i t y Tr

ansi t

io ns!

Thursday, November 14, 13

Custom Activity Transitions


Standard window animations
- default: scale/fade - customize: slide, fade, scale - Also thumbnail scale/crossfade

... But thats it Totally custom requires in-activity animations

44 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Custom Activity Transitions


Disable window animations Animate exiting activity Launch new activity with transparent window Animate content when activity comes up

45 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Thursday, November 14, 13

Grayscale thumbnails

ColorMatrix grayMatrix = new ColorMatrix(); grayMatrix.setSaturation(0); ColorMatrixColorFilter grayscaleFilter = new ColorMatrixColorFilter(grayMatrix); thumbnailDrawable.setColorFilter(grayscaleFilter);

47 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Drop shadow container


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(); } }
48 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Drop shadow depth


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

49 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Transparent activity background


<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>

50 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Launch sub-activity
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);
51 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Get animation start values

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

52 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Get animation end values


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; } });
53 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Animate thumbnail & description


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); } }); 54 #DV13 #FilthyRichAndroid
Thursday, November 14, 13

Fade in black background

ObjectAnimator.ofInt(mBackground, "alpha", 0, 255). start();

55 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Colorize thumbnail
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); }
56 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Animate drop shadow

ObjectAnimator shadowAnim = ObjectAnimator.ofFloat( mShadowLayout, "shadowDepth", 0, 1); shadowAnim.start();

57 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Animate back to main activity


@Override public void onBackPressed() { runExitAnimation(new Runnable() { public void run() { finish(); } }); } @Override public void finish() { super.finish(); overridePendingTransition(0, 0); }
58 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Fo ldi n

g L ay

ou t !

Thursday, November 14, 13

Fan Fare

60 #DV13 #FilthyRichAndroid Thursday, November 14, 13

61 #DV13 #FilthyRichAndroid Thursday, November 14, 13

M at r i x . s e tPo l yToPo

l y()

62 #DV13 #FilthyRichAndroid Thursday, November 14, 13

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

63 #DV13 #FilthyRichAndroid Thursday, November 14, 13

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

63 #DV13 #FilthyRichAndroid Thursday, November 14, 13

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

63 #DV13 #FilthyRichAndroid Thursday, November 14, 13

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

63 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Color Filters
Can be used to modify a shader Subclasses of ColorFilter
- ColorMatrixColorFilter - LightingColorFilter - PorterDu!ColorFilter

64 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Sepia Effect
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));

65 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Performance

Thursday, November 14, 13

Smoother is Better
Consistent frame rate Avoid hiccups Avoid large steps over few frames

67 #DV13 #FilthyRichAndroid Thursday, November 14, 13

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 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Avoid Overdraw
Developer options -> Show Overdraw Window background vs. opaque containers vs. opaque views

69 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Get Off that UI Thread!


Avoid expensive operations on UI thread
- network, database, bitmaps, ...

AsyncTask is your friend

70 #DV13 #FilthyRichAndroid Thursday, November 14, 13

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 #DV13 #FilthyRichAndroid Thursday, November 14, 13

clipPath
Not always the fastest way to clip to a path Doesnt support antialiasing Try BitmapShader

72 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Consider Time Travel


Go see Android Performance Workshop 2 days ago
- Memory - Performance tips - Tools - Case studies

73 #DV13 #FilthyRichAndroid Thursday, November 14, 13

For More Information


Romain:
curious-creature.org google.com/+RomainGuy @romainguy

Chet
graphics-geek.blogspot.com google.com/+ChetHaase @chethaase

Google I/O talks Parleys.com talks Devbytes on YouTube


74 #DV13 #FilthyRichAndroid Thursday, November 14, 13

Thursday, November 14, 13

Q&A

Filthy Rich Clients: Developing Animated and Graphical E!ects

75 #DV13 #FilthyRichAndroid

You might also like