Home | History | Annotate | Download | only in actions
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.photoeditor.actions;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.graphics.Canvas;
     22 import android.graphics.Paint;
     23 import android.graphics.drawable.Drawable;
     24 import android.util.AttributeSet;
     25 import android.view.MotionEvent;
     26 import android.view.View;
     27 import android.view.animation.AnimationUtils;
     28 
     29 import com.android.photoeditor.R;
     30 
     31 /**
     32  * Wheel that has a draggable thumb to set and get the normalized scale value from 0 to 1.
     33  */
     34 class ScaleWheel extends View {
     35 
     36     /**
     37      * Listens to scale changes.
     38      */
     39     public interface OnScaleChangeListener {
     40 
     41         void onProgressChanged(float progress, boolean fromUser);
     42     }
     43 
     44     private static final float MATH_PI = (float) Math.PI;
     45     private static final float MATH_HALF_PI = MATH_PI / 2;
     46 
     47     // Angles are defined between PI and -PI.
     48     private static final float ANGLE_SPANNED = MATH_PI * 4 / 3;
     49     private static final float ANGLE_BEGIN = ANGLE_SPANNED / 2.0f;
     50 
     51     private static final float THUMB_RADIUS_RATIO = 0.363f;
     52     private static final float INNER_RADIUS_RATIO = 0.24f;
     53 
     54     private final Drawable thumb;
     55     private final Drawable background;
     56     private final int thumbSize;
     57     private final Paint circlePaint;
     58     private final int maxProgress;
     59     private final float radiantInterval;
     60     private int thumbRadius;
     61     private int innerRadius;
     62     private int centerXY;
     63     private float angle;
     64     private int progress;
     65     private boolean dragThumb;
     66     private OnScaleChangeListener listener;
     67 
     68     public ScaleWheel(Context context, AttributeSet attrs) {
     69         super(context, attrs);
     70 
     71         Resources resources = context.getResources();
     72         thumbSize = (int) resources.getDimension(R.dimen.wheel_thumb_size);
     73 
     74         // Set the maximum progress and compute the radiant interval between progress values.
     75         maxProgress = 100;
     76         radiantInterval = ANGLE_SPANNED / maxProgress;
     77 
     78         thumb = resources.getDrawable(R.drawable.wheel_knot_selector);
     79         background = resources.getDrawable(R.drawable.scale_wheel_background);
     80         background.setAlpha(160);
     81 
     82         circlePaint = new Paint();
     83         circlePaint.setAntiAlias(true);
     84         circlePaint.setColor(resources.getColor(R.color.scale_wheel_interior_color));
     85     }
     86 
     87     public void setProgress(float progress) {
     88         if (updateProgress((int) (progress * maxProgress), false)) {
     89             updateThumbPositionByProgress();
     90         }
     91     }
     92 
     93     public void setOnScaleChangeListener(OnScaleChangeListener listener) {
     94         this.listener = listener;
     95     }
     96 
     97     @Override
     98     public void setVisibility(int visibility) {
     99         super.setVisibility(visibility);
    100 
    101         startAnimation(AnimationUtils.loadAnimation(getContext(),
    102                 (visibility == VISIBLE) ? R.anim.wheel_show : R.anim.wheel_hide));
    103     }
    104 
    105     @Override
    106     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    107         super.onSizeChanged(w, h, oldw, oldh);
    108 
    109         int wheelSize = Math.min(w, h);
    110         thumbRadius = (int) (wheelSize * THUMB_RADIUS_RATIO);
    111         innerRadius = (int) (wheelSize * INNER_RADIUS_RATIO);
    112 
    113         // The wheel would be centered at (centerXY, centerXY) and have outer-radius centerXY.
    114         centerXY = wheelSize / 2;
    115         updateThumbPositionByProgress();
    116     }
    117 
    118     @Override
    119     protected void onDraw(Canvas canvas) {
    120         super.onDraw(canvas);
    121 
    122         background.setBounds(0, 0, getWidth(), getHeight());
    123         background.draw(canvas);
    124 
    125         int thumbX = (int) (thumbRadius * Math.cos(angle) + centerXY);
    126         int thumbY = (int) (centerXY - thumbRadius * Math.sin(angle));
    127         int halfSize = thumbSize / 2;
    128         thumb.setBounds(thumbX - halfSize, thumbY - halfSize, thumbX + halfSize, thumbY + halfSize);
    129         thumb.draw(canvas);
    130 
    131         float radius = (progress * innerRadius) / maxProgress;
    132         canvas.drawCircle(centerXY, centerXY, radius, circlePaint);
    133     }
    134 
    135     @Override
    136     public boolean onTouchEvent(MotionEvent ev) {
    137         super.onTouchEvent(ev);
    138 
    139         if (isEnabled()) {
    140             switch (ev.getAction()) {
    141                 case MotionEvent.ACTION_DOWN:
    142                     updateThumbState(
    143                             isHittingThumbArea(ev.getX() - centerXY, centerXY - ev.getY()));
    144                     break;
    145 
    146                 case MotionEvent.ACTION_MOVE:
    147                     float x = ev.getX() - centerXY;
    148                     float y = centerXY - ev.getY();
    149                     if (!dragThumb && !updateThumbState(isHittingThumbArea(x, y))) {
    150                         // The thumb wasn't dragged and isn't being dragged, either.
    151                         break;
    152                     }
    153 
    154                     if (updateAngle(x, y)) {
    155                         int progress = (int) ((ANGLE_BEGIN - angle) / radiantInterval);
    156                         if (updateProgress(progress, true)) {
    157                             invalidate();
    158                         }
    159                     }
    160                     break;
    161 
    162                 case MotionEvent.ACTION_CANCEL:
    163                 case MotionEvent.ACTION_UP:
    164                     updateThumbState(false);
    165                     break;
    166             }
    167         }
    168         return true;
    169     }
    170 
    171     private boolean isHittingThumbArea(float x, float y) {
    172         double radius = Math.sqrt((x * x) + (y * y));
    173         return (radius > innerRadius) && (radius < centerXY);
    174     }
    175 
    176     private boolean updateAngle(float x, float y) {
    177         float angle;
    178         if (x == 0) {
    179             angle = (y >= 0) ? MATH_HALF_PI : -MATH_HALF_PI;
    180         } else {
    181             angle = (float) Math.atan(y / x);
    182         }
    183 
    184         if ((angle >= 0) && (x < 0)) {
    185             angle = angle - MATH_PI;
    186         } else if ((angle < 0) && (x < 0)) {
    187             angle = MATH_PI + angle;
    188         }
    189 
    190         if ((angle > ANGLE_BEGIN) || (angle <= ANGLE_BEGIN - ANGLE_SPANNED)) {
    191             return false;
    192         }
    193 
    194         this.angle = angle;
    195         return true;
    196     }
    197 
    198     private boolean updateProgress(int progress, boolean fromUser) {
    199         if ((this.progress != progress) && (progress >= 0) && (progress <= maxProgress)) {
    200             this.progress = progress;
    201 
    202             if (listener != null) {
    203                 listener.onProgressChanged((float) progress / maxProgress, fromUser);
    204             }
    205             return true;
    206         }
    207         return false;
    208     }
    209 
    210     private void updateThumbPositionByProgress() {
    211         angle = ANGLE_BEGIN - progress * radiantInterval;
    212         invalidate();
    213     }
    214 
    215     private boolean updateThumbState(boolean dragThumb) {
    216         if (this.dragThumb == dragThumb) {
    217             // The state hasn't been changed; no need for updates.
    218             return false;
    219         }
    220 
    221         this.dragThumb = dragThumb;
    222         thumb.setState(dragThumb ? PRESSED_ENABLED_STATE_SET : ENABLED_STATE_SET);
    223         invalidate();
    224         return true;
    225     }
    226 }
    227