1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.egg.octo; 16 17 import android.animation.TimeAnimator; 18 import android.content.Context; 19 import android.graphics.Canvas; 20 import android.graphics.ColorFilter; 21 import android.graphics.DashPathEffect; 22 import android.graphics.Matrix; 23 import android.graphics.Paint; 24 import android.graphics.Path; 25 import android.graphics.PixelFormat; 26 import android.graphics.PointF; 27 import android.graphics.Rect; 28 import android.graphics.drawable.Drawable; 29 import android.support.animation.DynamicAnimation; 30 import android.support.animation.SpringForce; 31 import android.support.annotation.NonNull; 32 import android.support.annotation.Nullable; 33 import android.support.animation.SpringAnimation; 34 import android.support.animation.FloatValueHolder; 35 36 public class OctopusDrawable extends Drawable { 37 private static float BASE_SCALE = 100f; 38 public static boolean PATH_DEBUG = false; 39 40 private static int BODY_COLOR = 0xFF101010; 41 private static int ARM_COLOR = 0xFF101010; 42 private static int ARM_COLOR_BACK = 0xFF000000; 43 private static int EYE_COLOR = 0xFF808080; 44 45 private static int[] BACK_ARMS = {1, 3, 4, 6}; 46 private static int[] FRONT_ARMS = {0, 2, 5, 7}; 47 48 private Paint mPaint = new Paint(); 49 private Arm[] mArms = new Arm[8]; 50 final PointF point = new PointF(); 51 private int mSizePx = 100; 52 final Matrix M = new Matrix(); 53 final Matrix M_inv = new Matrix(); 54 private TimeAnimator mDriftAnimation; 55 private boolean mBlinking; 56 private float[] ptmp = new float[2]; 57 private float[] scaledBounds = new float[2]; 58 randfrange(float a, float b)59 public static float randfrange(float a, float b) { 60 return (float) (Math.random()*(b-a) + a); 61 } clamp(float v, float a, float b)62 public static float clamp(float v, float a, float b) { 63 return v<a?a:v>b?b:v; 64 } 65 OctopusDrawable(Context context)66 public OctopusDrawable(Context context) { 67 float dp = context.getResources().getDisplayMetrics().density; 68 setSizePx((int) (100*dp)); 69 mPaint.setAntiAlias(true); 70 for (int i=0; i<mArms.length; i++) { 71 final float bias = (float)i/(mArms.length-1) - 0.5f; 72 mArms[i] = new Arm( 73 0,0, // arm will be repositioned on moveTo 74 10f*bias + randfrange(0,20f), randfrange(20f,50f), 75 40f*bias+randfrange(-60f,60f), randfrange(30f, 80f), 76 randfrange(-40f,40f), randfrange(-80f,40f), 77 14f, 2f); 78 } 79 } 80 setSizePx(int size)81 public void setSizePx(int size) { 82 mSizePx = size; 83 M.setScale(mSizePx/BASE_SCALE, mSizePx/BASE_SCALE); 84 // TaperedPathStroke.setMinStep(20f*BASE_SCALE/mSizePx); // nice little floaty circles 85 TaperedPathStroke.setMinStep(8f*BASE_SCALE/mSizePx); // classic tentacles 86 M.invert(M_inv); 87 } 88 startDrift()89 public void startDrift() { 90 if (mDriftAnimation == null) { 91 mDriftAnimation = new TimeAnimator(); 92 mDriftAnimation.setTimeListener(new TimeAnimator.TimeListener() { 93 float MAX_VY = 35f; 94 float JUMP_VY = -100f; 95 float MAX_VX = 15f; 96 private float ax = 0f, ay = 30f; 97 private float vx, vy; 98 long nextjump = 0; 99 long unblink = 0; 100 @Override 101 public void onTimeUpdate(TimeAnimator timeAnimator, long t, long dt) { 102 float t_sec = 0.001f * t; 103 float dt_sec = 0.001f * dt; 104 if (t > nextjump) { 105 vy = JUMP_VY; 106 nextjump = t + (long) randfrange(5000, 10000); 107 } 108 if (unblink > 0 && t > unblink) { 109 setBlinking(false); 110 unblink = 0; 111 } else if (Math.random() < 0.001f) { 112 setBlinking(true); 113 unblink = t + 200; 114 } 115 116 ax = (float) (MAX_VX * Math.sin(t_sec*.25f)); 117 118 vx = clamp(vx + dt_sec * ax, -MAX_VX, MAX_VX); 119 vy = clamp(vy + dt_sec * ay, -100*MAX_VY, MAX_VY); 120 121 // oob check 122 if (point.y - BASE_SCALE/2 > scaledBounds[1]) { 123 vy = JUMP_VY; 124 } else if (point.y + BASE_SCALE < 0) { 125 vy = MAX_VY; 126 } 127 128 point.x = clamp(point.x + dt_sec * vx, 0, scaledBounds[0]); 129 point.y = point.y + dt_sec * vy; 130 131 repositionArms(); 132 } 133 }); 134 } 135 mDriftAnimation.start(); 136 } 137 stopDrift()138 public void stopDrift() { 139 mDriftAnimation.cancel(); 140 } 141 142 @Override onBoundsChange(Rect bounds)143 public void onBoundsChange(Rect bounds) { 144 final float w = bounds.width(); 145 final float h = bounds.height(); 146 147 lockArms(true); 148 moveTo(w/2, h/2); 149 lockArms(false); 150 151 scaledBounds[0] = w; 152 scaledBounds[1] = h; 153 M_inv.mapPoints(scaledBounds); 154 } 155 156 // real pixel coordinates moveTo(float x, float y)157 public void moveTo(float x, float y) { 158 point.x = x; 159 point.y = y; 160 mapPointF(M_inv, point); 161 repositionArms(); 162 } 163 hitTest(float x, float y)164 public boolean hitTest(float x, float y) { 165 ptmp[0] = x; 166 ptmp[1] = y; 167 M_inv.mapPoints(ptmp); 168 return Math.hypot(ptmp[0] - point.x, ptmp[1] - point.y) < BASE_SCALE/2; 169 } 170 lockArms(boolean l)171 private void lockArms(boolean l) { 172 for (Arm arm : mArms) { 173 arm.setLocked(l); 174 } 175 } repositionArms()176 private void repositionArms() { 177 for (int i=0; i<mArms.length; i++) { 178 final float bias = (float)i/(mArms.length-1) - 0.5f; 179 mArms[i].setAnchor( 180 point.x+bias*30f,point.y+26f); 181 } 182 invalidateSelf(); 183 } 184 drawPupil(Canvas canvas, float x, float y, float size, boolean open, Paint pt)185 private void drawPupil(Canvas canvas, float x, float y, float size, boolean open, 186 Paint pt) { 187 final float r = open ? size*.33f : size * .1f; 188 canvas.drawRoundRect(x - size, y - r, x + size, y + r, r, r, pt); 189 } 190 191 @Override draw(@onNull Canvas canvas)192 public void draw(@NonNull Canvas canvas) { 193 canvas.save(); 194 { 195 canvas.concat(M); 196 197 // arms behind 198 mPaint.setColor(ARM_COLOR_BACK); 199 for (int i : BACK_ARMS) { 200 mArms[i].draw(canvas, mPaint); 201 } 202 203 // head/body/thing 204 mPaint.setColor(EYE_COLOR); 205 canvas.drawCircle(point.x, point.y, 36f, mPaint); 206 mPaint.setColor(BODY_COLOR); 207 canvas.save(); 208 { 209 canvas.clipOutRect(point.x - 61f, point.y + 8f, 210 point.x + 61f, point.y + 12f); 211 canvas.drawOval(point.x-40f,point.y-60f,point.x+40f,point.y+40f, mPaint); 212 } 213 canvas.restore(); 214 215 // eyes 216 mPaint.setColor(EYE_COLOR); 217 if (mBlinking) { 218 drawPupil(canvas, point.x - 16f, point.y - 12f, 6f, false, mPaint); 219 drawPupil(canvas, point.x + 16f, point.y - 12f, 6f, false, mPaint); 220 } else { 221 canvas.drawCircle(point.x - 16f, point.y - 12f, 6f, mPaint); 222 canvas.drawCircle(point.x + 16f, point.y - 12f, 6f, mPaint); 223 } 224 225 // too much? 226 if (false) { 227 mPaint.setColor(0xFF000000); 228 drawPupil(canvas, point.x - 16f, point.y - 12f, 5f, true, mPaint); 229 drawPupil(canvas, point.x + 16f, point.y - 12f, 5f, true, mPaint); 230 } 231 232 // arms in front 233 mPaint.setColor(ARM_COLOR); 234 for (int i : FRONT_ARMS) { 235 mArms[i].draw(canvas, mPaint); 236 } 237 238 if (PATH_DEBUG) for (Arm arm : mArms) { 239 arm.drawDebug(canvas); 240 } 241 } 242 canvas.restore(); 243 } 244 setBlinking(boolean b)245 public void setBlinking(boolean b) { 246 mBlinking = b; 247 invalidateSelf(); 248 } 249 250 @Override setAlpha(int i)251 public void setAlpha(int i) { 252 } 253 254 @Override setColorFilter(@ullable ColorFilter colorFilter)255 public void setColorFilter(@Nullable ColorFilter colorFilter) { 256 257 } 258 259 @Override getOpacity()260 public int getOpacity() { 261 return PixelFormat.TRANSLUCENT; 262 } 263 pathMoveTo(Path p, PointF pt)264 static Path pathMoveTo(Path p, PointF pt) { 265 p.moveTo(pt.x, pt.y); 266 return p; 267 } pathQuadTo(Path p, PointF p1, PointF p2)268 static Path pathQuadTo(Path p, PointF p1, PointF p2) { 269 p.quadTo(p1.x, p1.y, p2.x, p2.y); 270 return p; 271 } 272 mapPointF(Matrix m, PointF point)273 static void mapPointF(Matrix m, PointF point) { 274 float[] p = new float[2]; 275 p[0] = point.x; 276 p[1] = point.y; 277 m.mapPoints(p); 278 point.x = p[0]; 279 point.y = p[1]; 280 } 281 282 private class Link // he come to town 283 implements DynamicAnimation.OnAnimationUpdateListener { 284 final FloatValueHolder[] coords = new FloatValueHolder[2]; 285 final SpringAnimation[] anims = new SpringAnimation[coords.length]; 286 private float dx, dy; 287 private boolean locked = false; 288 Link next; 289 Link(int index, float x1, float y1, float dx, float dy)290 Link(int index, float x1, float y1, float dx, float dy) { 291 coords[0] = new FloatValueHolder(x1); 292 coords[1] = new FloatValueHolder(y1); 293 this.dx = dx; 294 this.dy = dy; 295 for (int i=0; i<coords.length; i++) { 296 anims[i] = new SpringAnimation(coords[i]); 297 anims[i].setSpring(new SpringForce() 298 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) 299 .setStiffness( 300 index == 0 ? SpringForce.STIFFNESS_LOW 301 : index == 1 ? SpringForce.STIFFNESS_VERY_LOW 302 : SpringForce.STIFFNESS_VERY_LOW/2) 303 .setFinalPosition(0f)); 304 anims[i].addUpdateListener(this); 305 } 306 } setLocked(boolean locked)307 public void setLocked(boolean locked) { 308 this.locked = locked; 309 } start()310 public PointF start() { 311 return new PointF(coords[0].getValue(), coords[1].getValue()); 312 } end()313 public PointF end() { 314 return new PointF(coords[0].getValue()+dx,coords[1].getValue()+dy); 315 } mid()316 public PointF mid() { 317 return new PointF( 318 0.5f*dx+(coords[0].getValue()), 319 0.5f*dy+(coords[1].getValue())); 320 } animateTo(PointF target)321 public void animateTo(PointF target) { 322 if (locked) { 323 setStart(target.x, target.y); 324 } else { 325 anims[0].animateToFinalPosition(target.x); 326 anims[1].animateToFinalPosition(target.y); 327 } 328 } 329 @Override onAnimationUpdate(DynamicAnimation dynamicAnimation, float v, float v1)330 public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float v, float v1) { 331 if (next != null) { 332 next.animateTo(end()); 333 } 334 OctopusDrawable.this.invalidateSelf(); 335 } 336 setStart(float x, float y)337 public void setStart(float x, float y) { 338 coords[0].setValue(x); 339 coords[1].setValue(y); 340 onAnimationUpdate(null, 0, 0); 341 } 342 } 343 344 private class Arm { 345 final Link link1, link2, link3; 346 float max, min; 347 Arm(float x, float y, float dx1, float dy1, float dx2, float dy2, float dx3, float dy3, float max, float min)348 public Arm(float x, float y, float dx1, float dy1, float dx2, float dy2, float dx3, float dy3, 349 float max, float min) { 350 link1 = new Link(0, x, y, dx1, dy1); 351 link2 = new Link(1, x+dx1, y+dy1, dx2, dy2); 352 link3 = new Link(2, x+dx1+dx2, y+dy1+dy2, dx3, dy3); 353 link1.next = link2; 354 link2.next = link3; 355 356 link1.setLocked(true); 357 link2.setLocked(false); 358 link3.setLocked(false); 359 360 this.max = max; 361 this.min = min; 362 } 363 364 // when the arm is locked, it moves rigidly, without physics setLocked(boolean locked)365 public void setLocked(boolean locked) { 366 link2.setLocked(locked); 367 link3.setLocked(locked); 368 } 369 setAnchor(float x, float y)370 private void setAnchor(float x, float y) { 371 link1.setStart(x,y); 372 } 373 getPath()374 public Path getPath() { 375 Path p = new Path(); 376 pathMoveTo(p, link1.start()); 377 pathQuadTo(p, link2.start(), link2.mid()); 378 pathQuadTo(p, link2.end(), link3.end()); 379 return p; 380 } 381 draw(@onNull Canvas canvas, Paint pt)382 public void draw(@NonNull Canvas canvas, Paint pt) { 383 final Path p = getPath(); 384 TaperedPathStroke.drawPath(canvas, p, max, min, pt); 385 } 386 387 private final Paint dpt = new Paint(); drawDebug(Canvas canvas)388 public void drawDebug(Canvas canvas) { 389 dpt.setStyle(Paint.Style.STROKE); 390 dpt.setStrokeWidth(0.75f); 391 dpt.setStrokeCap(Paint.Cap.ROUND); 392 393 dpt.setAntiAlias(true); 394 dpt.setColor(0xFF336699); 395 396 final Path path = getPath(); 397 canvas.drawPath(path, dpt); 398 399 dpt.setColor(0xFFFFFF00); 400 401 dpt.setPathEffect(new DashPathEffect(new float[] {2f, 2f}, 0f)); 402 403 canvas.drawLines(new float[] { 404 link1.end().x, link1.end().y, 405 link2.start().x, link2.start().y, 406 407 link2.end().x, link2.end().y, 408 link3.start().x, link3.start().y, 409 }, dpt); 410 dpt.setPathEffect(null); 411 412 dpt.setColor(0xFF00CCFF); 413 414 canvas.drawLines(new float[] { 415 link1.start().x, link1.start().y, 416 link1.end().x, link1.end().y, 417 418 link2.start().x, link2.start().y, 419 link2.end().x, link2.end().y, 420 421 link3.start().x, link3.start().y, 422 link3.end().x, link3.end().y, 423 }, dpt); 424 425 dpt.setColor(0xFFCCEEFF); 426 canvas.drawCircle(link2.start().x, link2.start().y, 2f, dpt); 427 canvas.drawCircle(link3.start().x, link3.start().y, 2f, dpt); 428 429 dpt.setStyle(Paint.Style.FILL_AND_STROKE); 430 canvas.drawCircle(link1.start().x, link1.start().y, 2f, dpt); 431 canvas.drawCircle(link2.mid().x, link2.mid().y, 2f, dpt); 432 canvas.drawCircle(link3.end().x, link3.end().y, 2f, dpt); 433 } 434 435 } 436 } 437