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 15package com.android.egg.octo; 16 17import android.animation.TimeAnimator; 18import android.content.Context; 19import android.graphics.Canvas; 20import android.graphics.ColorFilter; 21import android.graphics.DashPathEffect; 22import android.graphics.Matrix; 23import android.graphics.Paint; 24import android.graphics.Path; 25import android.graphics.PixelFormat; 26import android.graphics.PointF; 27import android.graphics.Rect; 28import android.graphics.drawable.Drawable; 29import android.support.animation.DynamicAnimation; 30import android.support.animation.SpringForce; 31import android.support.annotation.NonNull; 32import android.support.annotation.Nullable; 33import android.support.animation.SpringAnimation; 34import android.support.animation.FloatValueHolder; 35 36public 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 59 public static float randfrange(float a, float b) { 60 return (float) (Math.random()*(b-a) + a); 61 } 62 public static float clamp(float v, float a, float b) { 63 return v<a?a:v>b?b:v; 64 } 65 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 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 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 138 public void stopDrift() { 139 mDriftAnimation.cancel(); 140 } 141 142 @Override 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 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 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 171 private void lockArms(boolean l) { 172 for (Arm arm : mArms) { 173 arm.setLocked(l); 174 } 175 } 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 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 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 245 public void setBlinking(boolean b) { 246 mBlinking = b; 247 invalidateSelf(); 248 } 249 250 @Override 251 public void setAlpha(int i) { 252 } 253 254 @Override 255 public void setColorFilter(@Nullable ColorFilter colorFilter) { 256 257 } 258 259 @Override 260 public int getOpacity() { 261 return PixelFormat.TRANSLUCENT; 262 } 263 264 static Path pathMoveTo(Path p, PointF pt) { 265 p.moveTo(pt.x, pt.y); 266 return p; 267 } 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 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 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 } 307 public void setLocked(boolean locked) { 308 this.locked = locked; 309 } 310 public PointF start() { 311 return new PointF(coords[0].getValue(), coords[1].getValue()); 312 } 313 public PointF end() { 314 return new PointF(coords[0].getValue()+dx,coords[1].getValue()+dy); 315 } 316 public PointF mid() { 317 return new PointF( 318 0.5f*dx+(coords[0].getValue()), 319 0.5f*dy+(coords[1].getValue())); 320 } 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 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 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 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 365 public void setLocked(boolean locked) { 366 link2.setLocked(locked); 367 link3.setLocked(locked); 368 } 369 370 private void setAnchor(float x, float y) { 371 link1.setStart(x,y); 372 } 373 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 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(); 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