【问题标题】:How would I implement a swipe-based circular control like this?我将如何实现这样的基于滑动的循环控件?
【发布时间】:2014-04-13 12:07:54
【问题描述】:

我正在开发一个 Android 应用程序,我有一个 TextView,可以在其中显示价格(例如 50 美元)。

我想要一个类似这张图的圆形控件:

  • 在表盘上顺时针滑动手指可将金额增加 1 美元
  • 在表盘上逆时针滑动手指会减少数量 以 $1 步为单位

我做了一些研究,但找不到一个可行的实现。

您如何创建这样一个由滑动驱动的圆形控件?

【问题讨论】:

标签: android swipe


【解决方案1】:

DialView 类:

public abstract class DialView extends View {

    private float centerX;
    private float centerY;
    private float minCircle;
    private float maxCircle;
    private float stepAngle;

    public DialView(Context context) {
        super(context);
        stepAngle = 1;
        setOnTouchListener(new OnTouchListener() {
            private float startAngle;
            private boolean isDragging;
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                float touchX = event.getX();
                float touchY = event.getY();
                switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    startAngle = touchAngle(touchX, touchY);
                    isDragging = isInDiscArea(touchX, touchY);
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (isDragging) {
                        float touchAngle = touchAngle(touchX, touchY);
                        float deltaAngle = (360 + touchAngle - startAngle + 180) % 360 - 180;
                        if (Math.abs(deltaAngle) > stepAngle) {
                            int offset = (int) deltaAngle / (int) stepAngle;
                            startAngle = touchAngle;
                            onRotate(offset);
                        }
                    }
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    isDragging = false;
                    break;
                }
                return true;
            }
        });
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        centerX = getMeasuredWidth() / 2f;
        centerY = getMeasuredHeight() / 2f;
        super.onLayout(changed, l, t, r, b);
    }

    @SuppressLint("DrawAllocation")
    @Override
    protected void onDraw(Canvas canvas) {
        float radius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 2f;
        Paint paint = new Paint();
        paint.setDither(true);
        paint.setAntiAlias(true);
        paint.setStyle(Style.FILL);
        paint.setColor(0xFFFFFFFF);
        paint.setXfermode(null);
        LinearGradient linearGradient = new LinearGradient(
            radius, 0, radius, radius, 0xFFFFFFFF, 0xFFEAEAEA, Shader.TileMode.CLAMP);
        paint.setShader(linearGradient);
        canvas.drawCircle(centerX, centerY, maxCircle * radius, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        canvas.drawCircle(centerX, centerY, minCircle * radius, paint);
        paint.setXfermode(null);
        paint.setShader(null);
        paint.setColor(0x15000000);
        for (int i = 0, n =  360 / (int) stepAngle; i < n; i++) {
            double rad = Math.toRadians((int) stepAngle * i);
            int startX = (int) (centerX + minCircle * radius * Math.cos(rad));
            int startY = (int) (centerY + minCircle * radius * Math.sin(rad));
            int stopX = (int) (centerX + maxCircle * radius * Math.cos(rad));
            int stopY = (int) (centerY + maxCircle * radius * Math.sin(rad));
            canvas.drawLine(startX, startY, stopX, stopY, paint);
        }
        super.onDraw(canvas);
    }

    /**
     * Define the step angle in degrees for which the
     * dial will call {@link #onRotate(int)} event
     * @param angle : angle between each position
     */
    public void setStepAngle(float angle) {
        stepAngle = Math.abs(angle % 360);
    }

    /**
     * Define the draggable disc area with relative circle radius
     * based on min(width, height) dimension (0 = center, 1 = border)
     * @param radius1 : internal or external circle radius
     * @param radius2 : internal or external circle radius
     */
    public void setDiscArea(float radius1, float radius2) {
        radius1 = Math.max(0, Math.min(1, radius1));
        radius2 = Math.max(0, Math.min(1, radius2));
        minCircle = Math.min(radius1, radius2);
        maxCircle = Math.max(radius1, radius2);
    }

    /**
     * Check if touch event is located in disc area
     * @param touchX : X position of the finger in this view
     * @param touchY : Y position of the finger in this view
     */
    private boolean isInDiscArea(float touchX, float touchY) {
        float dX2 = (float) Math.pow(centerX - touchX, 2);
        float dY2 = (float) Math.pow(centerY - touchY, 2);
        float distToCenter = (float) Math.sqrt(dX2 + dY2);
        float baseDist = Math.min(centerX, centerY);
        float minDistToCenter = minCircle * baseDist;
        float maxDistToCenter = maxCircle * baseDist;
        return distToCenter >= minDistToCenter && distToCenter <= maxDistToCenter;
    }

    /**
     * Compute a touch angle in degrees from center
     * North = 0, East = 90, West = -90, South = +/-180
     * @param touchX : X position of the finger in this view
     * @param touchY : Y position of the finger in this view
     * @return angle
     */
    private float touchAngle(float touchX, float touchY) {
        float dX = touchX - centerX;
        float dY = centerY - touchY;
        return (float) (270 - Math.toDegrees(Math.atan2(dY, dX))) % 360 - 180;
    }

    protected abstract void onRotate(int offset);

}

使用它:

public class DialActivity extends Activity {

    @Override
    protected void onCreate(Bundle state) {
        setContentView(new RelativeLayout(this) {
            private int value = 0;
            private TextView textView;
            {
                addView(new DialView(getContext()) {
                    {
                        // a step every 20°
                        setStepAngle(20f);
                        // area from 30% to 90%
                        setDiscArea(.30f, .90f);
                    }
                    @Override
                    protected void onRotate(int offset) {
                        textView.setText(String.valueOf(value += offset));
                    }
                }, new RelativeLayout.LayoutParams(0, 0) {
                    {
                        width = MATCH_PARENT;
                        height = MATCH_PARENT;
                        addRule(RelativeLayout.CENTER_IN_PARENT);
                    }
                });
                addView(textView = new TextView(getContext()) {
                    {
                        setText(Integer.toString(value));
                        setTextColor(Color.WHITE);
                        setTextSize(30);
                    }
                }, new RelativeLayout.LayoutParams(0, 0) {
                    {
                        width = WRAP_CONTENT;
                        height = WRAP_CONTENT;
                        addRule(RelativeLayout.CENTER_IN_PARENT);
                    }
                });
            }
        });
        super.onCreate(state);
    }

}

结果:

【讨论】:

  • 感谢您花时间写这篇文章,但不幸的是,我已经尝试了 Maystro 的答案并且它奏效了。所以他应该得到答案。太糟糕了,我不能分配赏金,因为您的答案也有效。干杯
  • 没问题,我也需要它,但就我而言,带有超大的触控启动区。我分享了我的工作,也许其他人会感兴趣
【解决方案2】:

我已经修改了circularseekbar 的源代码,可以按照您的意愿工作。

你可以从modified cirucularseekbar获取mofidied类

首先在你的布局中包含控件并将你的表盘设置为背景

            <com.yourapp.CircularSeekBar
                android:id="@+id/circularSeekBar"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/amount_wheel_bg" />

然后,在您的活动中(它应该实现 OnCircularSeekBarChangeListener)添加以下内容:

//This is a reference to the layout control
private CircularSeekBar circularSeekBar;
//This is a reference to the textbox where you want to display the amount
private EditText amountEditText;
private int previousProgress = -1;

并添加如下回调方法:

@Override
   public void onProgressChanged(CircularSeekBar circularSeekBar,
                 int progress, boolean fromUser) {
          if(previousProgress == -1)
          {
                 //This is the first user touch we take it as a reference
                 previousProgress = progress;
          }
          else
          {
                 //The user is holding his finger down
                 if(progress == previousProgress)
                 {
                       //he is still in the same position, we don't do anything
                 }
                 else
                 {
                       //The user is moving his finger we need to get the differences

                       int difference = progress - previousProgress;                        

                       if(Math.abs(difference) > CircularSeekBar.DEFAULT_MAX/2)
                       {
                              //The user is at the top of the wheel he is either moving from the 0 -> MAx or Max -> 0
                              //We have to consider this as 1 step 

                              //to force it to be 1 unit and reverse sign;
                              difference /= Math.abs(difference); 
                              difference -= difference;

                       }                          
                       //update the amount
                       selectedAmount += difference;
                        previousProgress= progress;
                       updateAmountText();
                 }
          }

   }

   @Override
   public void onStopTrackingTouch(CircularSeekBar seekBar) {

          //reset the tracking progress
          previousProgress = -1;

   }

   @Override
   public void onStartTrackingTouch(CircularSeekBar seekBar) {

   }

   private void updateAmountText()
   {
          amountEditText.setText(String.format("%.2f", selectedAmount));
   }

selectedAmount 是一个 double 属性,用于存储所选金额。

希望对你有帮助。

【讨论】:

  • 修改后的 circularseekbar 源代码在哪里?您与 Dropbox 的链接似乎已损坏。
  • 截至今天(是的,很久以后)链接已损坏。有机会把它放在一个要点上吗?
【解决方案3】:

我刚刚写了下面的代码,只是理论上进行了测试。

private final double stepSizeAngle = Math.PI / 10f; //Angle diff to increase/decrease dial by 1$
private final double dialStartValue = 50.0;

//Center of your dial
private float dialCenterX = 500;
private float dialCenterY = 500;

private float fingerStartDiffX;
private float fingerStartDiffY;

private double currentDialValueExact = dialStartValue;


public boolean onTouchEvent(MotionEvent event) {
    int eventaction = event.getAction();

    switch (eventaction) {
        case MotionEvent.ACTION_DOWN: 
            //Vector between startpoint and center
            fingerStartDiffX = event.getX() - dialCenterX;
            fingerStartDiffY = event.getY() - dialCenterY;
            break;

        case MotionEvent.ACTION_MOVE:
            //Vector between current point and center
            float xDiff = event.getX() - dialCenterX;
            float yDiff = event.getY() - dialCenterY;

            //Range from -PI to +PI
            double alpha = Math.atan2(fingerStartDiffY, yDiff) - Math.atan2(fingerStartDiffX, xDiff);

            //calculate exact difference between last move and current move.
            //This will take positive and negative direction into account.
            double dialIncrease = alpha / stepSizeAngle;        
            currentDialValueExact += dialIncrease;

            //Round down if we're above the start value and up if we are below
            setDialValue((int)(currentDialValueExact > dialStartValue ? Math.floor(currentDialValueExact) : Math.ceil(currentDialValueExact));

            //set fingerStartDiff to the current position to allow multiple rounds on the dial
            fingerStartDiffX = xDiff;
            fingerStartDiffY = yDiff;
            break;
    }

    // tell the system that we handled the event and no further processing is required
    return true; 
}

private void setDialValue(int value) {
    //assign value
}

如果你想改变方向,只需alpha = -alpha

【讨论】:

  • 您能详细说明一下 dialCenterX 和 dialCenterY 吗?我怎样才能得到它们,它们的单位是什么?
  • dialCenterX 和 dialCenterY 是您用作界面的图像的圆的中心点坐标(您保留的示例图像)。
【解决方案4】:

也许您可以查看视图的onTouchEvent(MotionEvent)。移动手指时跟踪 x 和 y 坐标。请注意,当您移动手指时,坐标的模式会发生变化。您可以使用它来实现价格的增加/减少。看到这个link

【讨论】:

  • 我一开始就想到了这个解决方案,但结果证明它非常复杂,因为 x 和 y 移动会根据您滑动的轮子的哪个区域而有所不同。我无法得到正确的计算,所以我希望有一个更简单的方法,或者如果有人以前做过。
【解决方案5】:

您可以使用 Android SDK 示例中的 Android Gesture Builder。

我现在无法测试它,但您应该能够从示例创建应用程序,运行它,创建您想要的自定义手势(顺时针循环和逆时针循环),然后从设备/模拟器内部存储中获取手势原始文件(它是在你做出手势后由应用程序创建的)。

这样,您可以将其导入您的项目并使用手势库来拦截、注册和识别特定手势。您基本上添加了一个覆盖布局,您希望在其中捕获手势,然后您决定如何处理它。

在以下链接中查看更深入的分步指南:http://www.techotopia.com/index.php/Implementing_Android_Custom_Gesture_and_Pinch_Recognition

【讨论】:

    【解决方案6】:

    OvalSeekbar lib 做了类似的事情,我建议你看看它是如何完成运动事件的。这里是它的 git 的链接https://github.com/kshoji/AndroidCustomViews

    【讨论】:

      【解决方案7】:

      我编写了这个自定义 FrameLayout 来检测围绕其中心点的圆周运动。 我使用平面上三个点的方向以及它们之间的角度来确定用户何时在一个方向上绕了半圈,然后在同一方向上完成它。

      public class CircularDialView extends FrameLayout implements OnTouchListener {
          private TextView counter;
          private int count = 50;
      
          private PointF startTouch;
          private PointF currentTouch;
          private PointF center;
          private boolean turning;
          private boolean switched = false;
      
          public enum RotationOrientation {
              CW, CCW, LINEAR;
          }
          private RotationOrientation lastRotatationDirection;
      
          public CircularDialView(Context context) {
              super(context);
              init();
          }
      
          public CircularDialView(Context context, AttributeSet attrs) {
              super(context, attrs);
              init();
          }
      
          public CircularDialView(Context context, AttributeSet attrs, int defStyleAttr) {
              super(context, attrs, defStyleAttr);
              init();
          }
      
          private void init() {
              this.startTouch = new PointF();
              this.currentTouch = new PointF();
              this.center = new PointF();
              this.turning = false;
      
              this.setBackgroundResource(R.drawable.dial);
              this.counter = new TextView(getContext());
              this.counter.setTextSize(20);
              FrameLayout.LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
              params.gravity = Gravity.CENTER;
              addView(this.counter, params);
      
              updateCounter();
              this.setOnTouchListener(this);
          }
      
          private void updateCounter() {
              this.counter.setText(Integer.toString(count));
          }
      
          // need to keep the view square
          @Override
          public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
              super.onMeasure(widthMeasureSpec, widthMeasureSpec);
              center.set(getWidth()/2, getWidth()/2);
          }
      
          @Override
          public boolean onTouch(View v, MotionEvent event) {
              switch (event.getAction()) {
                  case MotionEvent.ACTION_DOWN: {
                      startTouch.set(event.getX(), event.getY());
                      turning = true;
                      return true; 
                  }
                  case MotionEvent.ACTION_MOVE: {
                      if(turning) {
                          currentTouch.set(event.getX(), event.getY());
                          RotationOrientation turningDirection = getOrientation(center, startTouch, currentTouch);
      
                          if (lastRotatationDirection != turningDirection) {
                              double angle = getRotationAngle(center, startTouch, currentTouch);
                              Log.d ("Angle", Double.toString(angle));
                              // the touch event has switched its orientation 
                              // and the current touch point is close to the start point
                              // a full cycle has been made
                              if (switched && angle < 10) {
                                  if (turningDirection == RotationOrientation.CCW) {
                                      count--;
                                      updateCounter();
                                      switched = false;
                                  }
                                  else if (turningDirection == RotationOrientation.CW) {
                                      count++;
                                      updateCounter();
                                      switched = false;
                                  }
                              }
                              // checking if the angle is big enough is needed to prevent
                              // the user from switching from the start point only
                              else if (!switched && angle > 170) {
                                  switched = true;
                              }
                          }
      
                          lastRotatationDirection = turningDirection;
                          return true;
                      }
                  }
      
                  case MotionEvent.ACTION_CANCEL:
                  case MotionEvent.ACTION_UP: {
                      turning  = false;
                      return true;
                  }
              }
      
              return false;
          }
      
      
          // checks the orientation of three points on a plane
          private RotationOrientation getOrientation(PointF a, PointF b, PointF c){
              double face = a.x * b.y + b.x * c.y + c.x * a.y - (c.x * b.y + b.x * a.y + a.x * c.y);
              if (face > 0)
                   return RotationOrientation.CW;
              else if (face < 0)
                   return RotationOrientation.CCW;
              else return RotationOrientation.LINEAR;
          }
      
          // using dot product to calculate the angle between the vectors ab and ac
          public double getRotationAngle(PointF a, PointF b, PointF c){
              double len1 = dist (a, b);
              double len2 = dist (a, c);
              double product = (b.x - a.x) * (c.x - a.x) + (b.y - a.y) * (c.y - a.y);
      
              return Math.toDegrees(Math.acos(product / (len1 * len2)));
          }
      
          // calculates the distance between two points on a plane
          public double dist (PointF a, PointF b) {
              return Math.sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
          }
      }
      

      【讨论】:

        【解决方案8】:

        我有一个朋友需要实现类似你想要的东西。

        他实际上使用了手势检测 - GestureOverlayViewMotionEvent

        通过创建自定义手势,他设法实现了这一点。

        我的朋友主要引用了this 网站。那里也有示例代码。

        希望你觉得这很有用!

        【讨论】:

          【解决方案9】:

          您需要使用 Circular SeekBar,您可以从 here 找到示例和库

          其他一些也可能有用herethis

          谢谢,希望对你有帮助。

          【讨论】:

            【解决方案10】:

            使用NumberPicker,后者更简单,然后customize it!

            【讨论】:

            • 这也不太友好
            • 如果是这种情况,您可以像这里描述的那样自定义它custom number picker
            • 手势是人体工程学的重要组成部分
            猜你喜欢
            • 1970-01-01
            • 2020-03-19
            • 1970-01-01
            • 2010-10-14
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-05-09
            • 2021-12-11
            相关资源
            最近更新 更多