/* * Copyright 2015 Daniel Dittmar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package dan.dit.whatsthat.system.store; import android.animation.ValueAnimator; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.support.v4.app.FragmentActivity; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import android.widget.Toast; import dan.dit.whatsthat.R; import dan.dit.whatsthat.achievement.AchievementProperties; import dan.dit.whatsthat.riddle.achievement.holders.MiscAchievementHolder; import dan.dit.whatsthat.testsubject.TestSubject; import dan.dit.whatsthat.util.image.ColorAnalysisUtil; import dan.dit.whatsthat.util.image.ImageUtil; /** * Created by daniel on 13.06.15. */ public class AboutView extends View implements StoreContainer { private static final int[] COLORS = new int[] {0xFFFF0000, 0xFFfa7600, 0xFFffba00, 0xFF0dd70d, 0xFF0d4fd7, 0xFF9f00b5, 0xFF921126, 0xFFe5dd00, 0xFF00c3d4}; private static final int AREA_51_INDEX = 5; private static final int AREA_51_HACK = 137; private float mDiskAngle; private Paint mDiskPaint; private RectF mDiskBound; private Path mTextPath; private Paint mTextPaint; private Rect mTextBounds; private float mCenterX; private float mCenterY; private float mRadius; private float mAngleDelta; private int mNopeCount; private float mTextPaintDefaultSize; private VelocityTracker mSpinTracker; private float mSpinDownAngleRad; private float mSpinDownDiskAngle; private ValueAnimator mSpinAnimator; public AboutView(Context context, AttributeSet attrs) { super(context, attrs); setWillNotDraw(false); init(); } private void init() { mDiskPaint = new Paint(); mDiskPaint.setAntiAlias(true); mDiskPaint.setStyle(Paint.Style.FILL); mDiskBound = new RectF(); mTextPath = new Path(); mTextPaint = new Paint(); mTextPaintDefaultSize = ImageUtil.convertDpToPixel(18.f, getResources().getDisplayMetrics ().densityDpi); mTextPaint.setTextSize(mTextPaintDefaultSize); mTextPaint.setAntiAlias(true); setLayerType(View.LAYER_TYPE_SOFTWARE, null); setDiskAngleGrad(30.f); mTextBounds = new Rect(); setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { final float yDelta = motionEvent.getY() - mCenterY; final float xDelta = motionEvent.getX() - mCenterX; boolean isInsideCircle = mRadius >= 0.f && mAngleDelta > 0.f && xDelta * xDelta + yDelta * yDelta <= mRadius * mRadius; if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) { if (mSpinTracker != null) { mSpinTracker.addMovement(motionEvent); } startSpinning(motionEvent.getX(), motionEvent.getY()); if (isInsideCircle && mSpinAnimator == null) { float angle = getDiskGradAngleBetweenPoints(0, 0, xDelta, yDelta); int index = (int) (angle / mAngleDelta); startFeedback(index); } } else if (isInsideCircle && motionEvent.getActionMasked() == MotionEvent .ACTION_DOWN) { // pressing down inside circle, pressing down outside doesnt influence anything cancelSpinning(); if (mSpinTracker != null) { mSpinTracker.recycle(); } mSpinTracker = VelocityTracker.obtain(); mSpinTracker.addMovement(motionEvent); mSpinDownAngleRad = getAngleBetweenPoints(0, 0, xDelta, yDelta, true); mSpinDownDiskAngle = mDiskAngle; return true; // else no ACTION_UP will be delivered } else if (motionEvent.getActionMasked() == MotionEvent.ACTION_MOVE) { if (mSpinTracker != null) { mSpinTracker.addMovement(motionEvent); } if (!isInsideCircle) { startSpinning(motionEvent.getX(), motionEvent.getY()); } else { cancelSpinning(); float currAngle = getAngleBetweenPoints(0, 0, xDelta, yDelta, true); float angleDiff = currAngle - mSpinDownAngleRad; // how much difference // between original down action and now in angle radians setDiskAngleGrad(mSpinDownDiskAngle + (float) (angleDiff / Math.PI * 180)); invalidate(); } } return false; } }); } private void cancelSpinning() { if (mSpinAnimator != null) { Log.d("Riddle", "Cancel spinning."); mSpinAnimator.cancel(); } mSpinAnimator = null; } private void startSpinning(float x, float y) { if (mSpinTracker == null) { return; } cancelSpinning(); // use gram schmidt orthogonalization to obtain orthogonal part of velocity to current // position x/y on disk float rx = x - mCenterX; float ry = y - mCenterY; mSpinTracker.computeCurrentVelocity(1); // pixels per millisecond float vx = mSpinTracker.getXVelocity(); float vy = mSpinTracker.getYVelocity(); float factor = (rx * vx + ry * vy) / (rx * rx + ry * ry); float spinX = vx - factor * rx; float spinY = vy - factor * ry; // spin is now orthogonal to r, that is spinX * rx + spinY * ry = 0 // now only direction of spinning (CW, CCW) is missing and obtained by angle of spinning // direction. But be ware that the atan angle function only returns angles between -pi // and pi and results are equal for left and right half of disk float direction = getAngleBetweenPoints(spinX + rx, spinY + ry, rx, ry, false); float signum = direction >= 0f ? -1f : 1f; if (rx <= 0) { signum = -signum; // invert for left half of disk } // animate that angle speed to zero and spin the disk float startAngleSpeed = signum * (float) (Math.sqrt(spinX * spinX + spinY * spinY)); if (Math.abs(startAngleSpeed) > 0) { mSpinAnimator = ValueAnimator.ofFloat(startAngleSpeed, 0); mSpinAnimator.setInterpolator(new DecelerateInterpolator()); mSpinAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { public long mLastPlayTime; @Override public void onAnimationUpdate(ValueAnimator animation) { long playTimeDelta = animation.getCurrentPlayTime() - mLastPlayTime; mLastPlayTime = animation.getCurrentPlayTime(); setDiskAngleGrad(mDiskAngle + playTimeDelta * (Float) mSpinAnimator .getAnimatedValue()); invalidate(); } }); Log.d("HomeStuff", "Starting animation with start angle speed: " + startAngleSpeed); mSpinAnimator.setDuration(Math.max(100L, (long) Math.abs(startAngleSpeed * 10000))); mSpinAnimator.start(); if (TestSubject.isInitialized()) { TestSubject.getInstance().getAchievementHolder().getMiscData().putValue (MiscAchievementHolder.KEY_SPIN_WHEEL_START_ANGLE_SPEED, (long) (startAngleSpeed * 1000), AchievementProperties.UPDATE_POLICY_ALWAYS); } } mSpinTracker.recycle(); mSpinTracker = null; } private void setDiskAngleGrad(float angleGrad) { mDiskAngle = angleGrad; if (mDiskAngle < 0f) { mDiskAngle += 360f; } else if (mDiskAngle >= 360f) { mDiskAngle -= 360f; } } private float getDiskGradAngleBetweenPoints(float x1, float y1, float x2, float y2) { float angle = 180.f * (float) (Math.atan2(y2 - y1, x2 - x1) / Math.PI); angle -= mDiskAngle; if (angle < 0.f) { angle += 360.f; } return angle; } private float getAngleBetweenPoints(float x1, float y1, float x2, float y2, boolean normalize) { float angle = (float) (Math.atan2(y2 - y1, x2 - x1)); if (normalize && angle < 0.f) { angle += 2 * Math.PI; } return angle; } private void startFeedback(int index) { if (index == AREA_51_INDEX) { mNopeCount++; if (mNopeCount >= AREA_51_HACK) { Toast.makeText(getContext(), "Clicking " + AREA_51_HACK + " times resolved bug!", Toast.LENGTH_LONG).show(); mNopeCount = 0; } else { Toast.makeText(getContext(), "Bug", Toast.LENGTH_SHORT).show(); return; } } String[] emailSubject = getResources().getStringArray(R.array.contact_mail_subject); String[] emailBody = getResources().getStringArray(R.array.contact_mail_body); if (index >= 0 && index < emailSubject.length) { try { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("plain/text"); intent.putExtra(Intent.EXTRA_EMAIL, new String[]{TestSubject.EMAIL_FEEDBACK}); intent.putExtra(Intent.EXTRA_SUBJECT, emailSubject[index]); if (index < emailBody.length) { intent.putExtra(Intent.EXTRA_TEXT, emailBody[index]); } getContext().startActivity(intent); } catch (ActivityNotFoundException e) { Toast.makeText(getContext(), "Nothing to send.", Toast.LENGTH_SHORT).show(); } } } @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); mCenterX = canvas.getWidth() / 2.f; mCenterY = canvas.getHeight() / 2.f; float border = 2.f; mRadius = Math.min(mCenterX, mCenterY) - border; mDiskBound.set(mCenterX - mRadius, mCenterY - mRadius, mCenterX + mRadius, mCenterY + mRadius); mDiskPaint.setColor(Color.BLACK); canvas.drawCircle(mCenterX, mCenterY, mRadius + border, mDiskPaint); String[] contactTexts = getResources().getStringArray(R.array.contact_headings); float currAngle = mDiskAngle; mAngleDelta = 360.f / (float) contactTexts.length; int index = 0; for (String s : contactTexts) { int color = COLORS[index % COLORS.length]; mDiskPaint.setColor(color); canvas.drawArc(mDiskBound, currAngle, mAngleDelta, true, mDiskPaint); mTextPath.rewind(); mTextPaint.setTextSize(mTextPaintDefaultSize); final float maxTextLengthFactor = 0.70f; do { mTextPaint.getTextBounds(s, 0, s.length(), mTextBounds); if (mTextBounds.width() > mRadius * maxTextLengthFactor) { mTextPaint.setTextSize(mTextPaint.getTextSize() - 1.f); // linear search for // fitting size, not too important to optimize this to logarithmic } } while (mTextBounds.width() > mRadius * maxTextLengthFactor && mTextPaint.getTextSize() > 1.f); final float textStartOffset = (mRadius - mTextBounds.width()) / 2.f; final float textAngleRad = (float) ((currAngle + mAngleDelta / 2.f)/ 180.f * Math.PI); final float angleCos = (float) Math.cos(textAngleRad); final float angleSin = (float) Math.sin(textAngleRad); if (angleCos < 0.f) { float pathOffsetX = angleSin * mTextBounds.height() / 3; float pathOffsetY = -angleCos * mTextBounds.height() / 3; // in left half we want text to go from outer to inner mTextPath.moveTo(mCenterX + (mRadius - textStartOffset) * angleCos + pathOffsetX, mCenterY + (mRadius - textStartOffset) * angleSin + pathOffsetY); mTextPath.lineTo(mCenterX + pathOffsetX, mCenterY + pathOffsetY); } else { //offset so that approximately the baseline of the text is in on the path // dividing the cake piece float pathOffsetX = -angleSin * mTextBounds.height() / 3; float pathOffsetY = angleCos * mTextBounds.height() / 3; // in right half we want text to go from inner to outer mTextPath.moveTo(mCenterX + textStartOffset * angleCos + pathOffsetX, mCenterY + textStartOffset * angleSin + pathOffsetY); mTextPath.lineTo(mCenterX + mRadius * angleCos + pathOffsetX, mCenterY + mRadius * angleSin + pathOffsetY); } mTextPaint.setColor(getInverseColor(color)); canvas.drawTextOnPath(s, mTextPath, 0.f, 0.f, mTextPaint); currAngle += mAngleDelta; index++; } } private int getInverseColor(int fromColor) { if (ColorAnalysisUtil.getBrightnessNoAlpha(fromColor) >= 0.3) { return Color.BLACK; } else { return Color.WHITE; } } @Override public void refresh(StoreActivity activity, FrameLayout titleBackContainer) { requestLayout(); invalidate(); } @Override public void stop(FragmentActivity activity, boolean pausedOnly) { if (mSpinAnimator != null) { mSpinAnimator.cancel(); mSpinAnimator = null; } } @Override public View getView() { return getRootView(); } }