How to move image on curve on touch event in andro

2019-02-28 10:38发布

问题:

I have drawn a Cubic Curve on canvas using

 myPath.cubicTo(10, 10, w, h/2, 10, h-10);

I have four ImageView on that screen and I want to move that ImageViews on the drawn curve when I drag that image with touch.

I have referred the links :

Move Image on Curve Path

Move object on Curve

Move imageview on curve

What I get is, Animation to move the Image on Curve with the duration defined by t. But I want to move that ImageView on touch in direction of that curve area only.

Following is my Screen :

So, I want all the (x,y) co-ordinates of the curve to move ImageView on that curve only.

Else I want an equation to draw a curve so that I can interpolate x value for the touched y value.

I have goggled a lot but didn't succeed. Any advice or guidance will help me a lot.

回答1:

Approach

I would suggest a different approach than using bezier as you would need to reproduce the math for it in order to get the positions.

By using simple trigonometry you can achieve the same visual result but in addition have full control of the positions.

Trigonometry

For example:

THIS ONLINE DEMO produces this result (simplified version for sake of demo):

Define an array with the circles and angle positions instead of y and x positions. You can filter angles later if they (e.g. only show angles between -90 and 90 degrees).

Using angles will make sure they stay ordered when moved.

var balls = [-90, -45, 0, 45];  // example "positions"

To replace the Bezier curve you can do this instead:

/// some setup variables
var xCenter = -80,                  /// X center of circle
    yCenter = canvas.height * 0.5,  /// Y center of circle
    radius = 220,                   /// radius of circle
    x, y;                           /// to calculate line position

/// draw half circle
ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI);

ctx.stroke();

Now we can use an Y value from mouse move/touch etc. to move around the circles:

/// for demo, mousemove - adopt as needed for touch
canvas.onmousemove = function(e) {

    /// get Y position which is used as delta to angle
    var rect = demo.getBoundingClientRect();

    dlt = e.clientY - rect.top;

    /// render the circles in new positions        
    render();
}

The rendering iterates through the balls array and render them in their angle + delta:

for(var i = 0, angle; i < balls.length; i++) {
    angle = balls[i];
    pos = getPosfromAngle(angle);

    /// draw circles etc. here
}

The magic function is this:

function getPosfromAngle(a) {

    /// get angle from circle and add delta
    var angle = Math.atan2(delta - yCenter, radius) + a * Math.PI / 180;

    return [xCenter + radius * Math.cos(angle),
            yCenter + radius * Math.sin(angle)];
}

radius is used as a pseudo position. You can replace this with an actual X position but is frankly not needed.

In this demo, to keep it simple, I have only attached mouse move. Move the mouse over the canvas to see the effect.

As this is demo code it's not structured optimal (separate render of background and the circles etc.).

Feel free to adopt and modify to suit your needs.



回答2:

This code I have used to achieve this functionality and it works perfect as per your requirement...

public class YourActivity extends Activity {


    private class ButtonInfo {
        public Button btnObj;
        public PointF OrigPos;
        public double origAngle;
        public double currentAngle;
        public double minAngle;
        public double maxAngle;
        boolean isOnClick = false;
    }

    private int height;
    private double radius;
    private PointF centerPoint;

    private final int NUM_BUTTONS = 4;
    private final int FIRST_INDEX = 0;
    private final int SECOND_INDEX = 1;
    private final int THIRD_INDEX = 2;
    private final int FORTH_INDEX = 3;

    private final String FIRST_TAG = "FiRST_BUTTON";
    private final String SECOND_TAG = "SECOND_BUTTON";
    private final String THIRD_TAG = "THIRD_BUTTON";
    private final String FORTH_TAG = "FORTH_BUTTON";

    private boolean animInProgress = false;
    private int currentButton = -1;

    private ButtonInfo[] buttonInfoArray = new ButtonInfo[NUM_BUTTONS];

    private int curveImageResource = -1;

    private RelativeLayout parentContainer;
    private int slop;

    private boolean initFlag = false;

    private int touchDownY = -1;
    private int touchDownX = -1;
    private int animCount;

    private Context context;

    @SuppressWarnings("deprecation")
    @SuppressLint("NewApi")
    @Override
    protected void onCreate(Bundle savedInstanceState) {


        super.onCreate(savedInstanceState);
        overridePendingTransition(R.anim.fadeinleft, R.anim.fadeoutleft);
        // hide action bar in view
        this.requestWindowFeature(Window.FEATURE_NO_TITLE);

        Thread.setDefaultUncaughtExceptionHandler(
                new MyDefaultExceptionHandler(this, getLocalClassName()));

        setContentView(R.layout.your_layout);



        context = this;

        final ImageView curve_image = (ImageView) findViewById(R.id.imageView1);
        parentContainer = (RelativeLayout) findViewById(R.id.llView);

        // Set buttons on their location
        for (int i = 0; i < NUM_BUTTONS; i++) {
            buttonInfoArray[i] = new ButtonInfo();
        }

        Button img1 = (Button) findViewById(R.id.button_option1);
        Button img2 = (Button) findViewById(R.id.button_option2);
        Button img3 = (Button) findViewById(R.id.button_option3);
        Button img4 = (Button) findViewById(R.id.button_option4);


        //1st button

        buttonInfoArray[FIRST_INDEX].btnObj = (Button) this
                .findViewById(R.id.setting_button_option);
        buttonInfoArray[FIRST_INDEX].btnObj.setTag(FIRST_TAG);




        // 2nd button
        buttonInfoArray[SECOND_INDEX].btnObj = (Button) this
                .findViewById(R.id.scanning_button_option);
        buttonInfoArray[SECOND_INDEX].btnObj.setTag(SECOND_TAG);

        // 3rd button
        buttonInfoArray[THIRD_INDEX].btnObj = (Button) this
                .findViewById(R.id.manual_button_option);
        buttonInfoArray[THIRD_INDEX].btnObj.setTag(THIRD_TAG);

        // 4th button
        buttonInfoArray[FORTH_INDEX].btnObj = (Button) this
                .findViewById(R.id.logout_button_option);
        buttonInfoArray[FORTH_INDEX].btnObj.setTag(FORTH_TAG);

        for (ButtonInfo currentButtonInfo : buttonInfoArray) {
            currentButtonInfo.btnObj.setClickable(false);
        }
        for (ButtonInfo currentButtonInfo : buttonInfoArray) {
            currentButtonInfo.btnObj.bringToFront();
        }

        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);


        ViewTreeObserver vtoLayout = parentContainer.getViewTreeObserver();
        vtoLayout.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {

            @Override
            public void onGlobalLayout() {
                if (initFlag == true)
                    return;

                centerPoint = new PointF(0, (parentContainer.getHeight()) / 2);

                curve_image.setImageResource(curveImageResource);

                ViewTreeObserver vtoCurveImage = curve_image
                        .getViewTreeObserver();
                vtoCurveImage
                        .addOnGlobalLayoutListener(new OnGlobalLayoutListener() {

                            @Override
                            public void onGlobalLayout() {


                                if (initFlag == true)
                                    return;

                                ViewConfiguration vc = ViewConfiguration.get(parentContainer
                                        .getContext());
                                slop = vc.getScaledTouchSlop();
                                parentContainer.setOnTouchListener(tlobj);


                                height = curve_image.getMeasuredHeight();
                                curve_image.getMeasuredWidth();

                                radius = (height / 2);

                                double angleDiff = Math.PI / (NUM_BUTTONS + 1);
                                double initialAngle = (Math.PI / 2 - angleDiff);
                                for (ButtonInfo currentButtonInfo : buttonInfoArray) {
                                    currentButtonInfo.origAngle = initialAngle;
                                    initialAngle -= angleDiff;
                                }

                                double tempCurrentAngle;
                                double maxAngle = (-1 * Math.PI / 2);
                                tempCurrentAngle = maxAngle;
                                for (int i = NUM_BUTTONS - 1; i >= 0; i--) {
                                    buttonInfoArray[i].maxAngle = tempCurrentAngle;

                                    int buttonHeight = buttonInfoArray[i].btnObj
                                            .getHeight();

                                    if (buttonHeight < 30) {
                                        buttonHeight = 80;
                                    }

                                    tempCurrentAngle = findNextMaxAngle(
                                            tempCurrentAngle,
                                            (buttonHeight + 5));
                                }

                                double minAngle = (Math.PI / 2);
                                tempCurrentAngle = minAngle;
                                for (int i = 0; i < NUM_BUTTONS; i++) {
                                    buttonInfoArray[i].minAngle = tempCurrentAngle;

                                    int buttonHeight = buttonInfoArray[i].btnObj
                                            .getHeight();

                                    if (buttonHeight < 30) {
                                        buttonHeight = 80;
                                    }
                                    tempCurrentAngle = findNextMinAngle(
                                            tempCurrentAngle, (buttonHeight + 5));
                                }

                                for (ButtonInfo currentButtonInfo : buttonInfoArray) {

                                    PointF newPos = getPointByAngle(currentButtonInfo.origAngle);

                                    currentButtonInfo.OrigPos = newPos;
                                    currentButtonInfo.currentAngle = currentButtonInfo.origAngle;
                                    setTranslationX(
                                            currentButtonInfo.btnObj,
                                            (int) currentButtonInfo.OrigPos.x - 50);
                                    setTranslationY(
                                            currentButtonInfo.btnObj,
                                            (int) currentButtonInfo.OrigPos.y - 50);
                                    currentButtonInfo.btnObj.requestLayout();
                                }

                                initFlag = true;
                            }

                        });

            }

        });


    }

    /**
     * Find next max angle
     * @param inputAngle
     * @param yDist
     * @return
     */
    private double findNextMaxAngle(double inputAngle, int yDist) {
        float initYPos = (float) (centerPoint.y - (Math.sin(inputAngle) * radius));

        float finalYPos = initYPos - yDist;

        float finalXPos = getXPos(finalYPos);

        double newAngle = getNewAngle(new PointF(finalXPos, finalYPos));
        return newAngle;
    }

    /**
     * Find next min angle
     * @param inputAngle
     * @param yDist
     * @return
     */
    private double findNextMinAngle(double inputAngle, int yDist) {
        float initYPos = (int) (centerPoint.y - (Math.sin(inputAngle) * radius));

        float finalYPos = initYPos + yDist;

        float finalXPos = getXPos(finalYPos);

        double newAngle = getNewAngle(new PointF(finalXPos, finalYPos));
        return newAngle;
    }

    /**
     * Apply reset transformation when user release touch
     * @param buttonInfoObj
     */
    public void applyResetAnimation(final ButtonInfo buttonInfoObj) {
        ValueAnimator animator = ValueAnimator.ofFloat(0, 1); // values from 0
                                                                // to 1
        animator.setDuration(1000); // 5 seconds duration from 0 to 1

        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = ((Float) (animation.getAnimatedValue()))
                        .floatValue();
                // Set translation of your view here. Position can be calculated
                // out of value. This code should move the view in a half
                // circle.

                double effectiveAngle = buttonInfoObj.origAngle
                        + ((buttonInfoObj.currentAngle - buttonInfoObj.origAngle) * (1.0 - value));

                PointF newPos = getPointByAngle(effectiveAngle);

                setTranslationX(buttonInfoObj.btnObj, newPos.x - 50);
                setTranslationY(buttonInfoObj.btnObj, newPos.y - 50);

            }
        });

        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                animCount++;
                if (animCount == NUM_BUTTONS) {
                    animCount = 0;
                    currentButton = -1;
                    animInProgress = false;

                    for (ButtonInfo currentButtonInfo : buttonInfoArray) {
                        setTranslationX(currentButtonInfo.btnObj,
                                currentButtonInfo.OrigPos.x - 50);
                        setTranslationY(currentButtonInfo.btnObj,
                                currentButtonInfo.OrigPos.y - 50);

                        currentButtonInfo.isOnClick = false;
                        currentButtonInfo.currentAngle = currentButtonInfo.origAngle;
                        currentButtonInfo.btnObj.setPressed(false);
                        currentButtonInfo.btnObj.requestLayout();
                    }

                }

            }
        });
        animator.start();
    }



    /**
     * On Touch start animation
     */
    private OnTouchListener tlobj = new OnTouchListener() {

        @Override
        public boolean onTouch(View v, MotionEvent motionEvent) {


            switch (MotionEventCompat.getActionMasked(motionEvent)) {
            case MotionEvent.ACTION_MOVE:

                if (currentButton < 0) {
                    return false;
                }
                if (animInProgress == true) {
                    return true;
                }

                float delta_y = motionEvent.getRawY() - touchDownY;
                float delta_x = motionEvent.getRawX() - touchDownX;

                updateButtonPos(new PointF((int) delta_x, (int) delta_y));

                if (Math.abs(delta_x) > slop || Math.abs(delta_y) > slop) {
                    buttonInfoArray[currentButton].isOnClick = false;
                    parentContainer.requestDisallowInterceptTouchEvent(true);
                }
                return true;
            case MotionEvent.ACTION_UP:
                animCount = 0;
                if (currentButton < 0) {
                    return false;
                }

                if(animInProgress == true) {
                    return true;
                }
                animInProgress = true;
                for (ButtonInfo currentButtonInfo : buttonInfoArray) {
                    applyResetAnimation(currentButtonInfo);
                    if (currentButtonInfo.isOnClick) {
                        // TODO onClick code
                        String currentTag = (String) currentButtonInfo.btnObj.getTag();

                        if(currentTag.equalsIgnoreCase(FIRST_TAG)) {
                            //handle first button click
                        } else if(currentTag.equalsIgnoreCase(SECOND_TAG)) {
                            //handle second button click
                        } else if(currentTag.equalsIgnoreCase(THIRD_TAG)) {
                            //handle third button click
                        } else if(currentTag.equalsIgnoreCase(FORTH_TAG)) {
                            //handle forth button click
                        } 
                    }
                }
                return true;
            case MotionEvent.ACTION_DOWN:

                if (currentButton >= 0) {
                    return false;
                }

                if (animInProgress == true) {
                    return true;
                }

                animCount = 0;

                int buttonIndex = 0;
                for (buttonIndex = 0; buttonIndex < NUM_BUTTONS; buttonIndex++) {
                    final ButtonInfo currentButtonInfo = buttonInfoArray[buttonIndex];

                    if (isRectHit(currentButtonInfo.btnObj, motionEvent,
                            currentButtonInfo.OrigPos)) {
                        currentButton = buttonIndex;
                        touchDownX = (int) motionEvent.getRawX();
                        touchDownY = (int) motionEvent.getRawY();
                        currentButtonInfo.isOnClick = true;
                        currentButtonInfo.btnObj.setPressed(true);

                        break;
                    }

                }
                if (buttonIndex == NUM_BUTTONS) {
                    currentButton = -1;
                }

                break;
            default:
                break;
            }
            return false;
        }

    };

    /**
     * Get X POS
     * @param yPos
     * @return
     */
    public float getXPos(float yPos) {
        float xPos = (float) (centerPoint.x
                + Math.sqrt((radius * radius)
                        - ((yPos - centerPoint.y) * (yPos - centerPoint.y))));
        return xPos;
    }

    /**
     * Get YPos based on X
     * @param xPos
     * @param isPositive
     * @return
     */
    public float getYPos(float xPos, boolean isPositive) {
        if (isPositive)
            return (float) (centerPoint.y - Math.sqrt((radius * radius)
                    - ((xPos - centerPoint.x) * (xPos - centerPoint.x))));
        else
            return (float) (centerPoint.y + Math.sqrt((radius * radius)
                    - ((xPos - centerPoint.x) * (xPos - centerPoint.x))));
    }

    /**
     * Get New angle from define point
     * @param newPoint
     * @return
     */
    private double getNewAngle(PointF newPoint) {

        double deltaY = newPoint.y - centerPoint.y;
        double deltaX = newPoint.x - centerPoint.x;

        double newPointAngle = Math.atan(-1.0 * deltaY / deltaX);
        return newPointAngle;
    }

    /**
     * get Point By Angle
     * @param angle
     * @return
     */
    private PointF getPointByAngle(double angle) {

        PointF newPos;
        double newX = centerPoint.x + Math.cos(angle) * radius;
        double newY = (centerPoint.y) - (Math.sin(angle) * radius);
        newPos = new PointF((int) newX, (int) newY);

        return newPos;
    }

    /**
     * Set new location for passed button
     * @param currentButtonIndex
     * @param effectiveDelta
     * @param percentageCompleted
     * @return
     */
    private double updateControl(int currentButtonIndex, PointF effectiveDelta,
            double percentageCompleted) {

        PointF newPos = new PointF();
        StringBuilder s1 = new StringBuilder();

        double maxAngleForCurrentButton = buttonInfoArray[currentButtonIndex].maxAngle;
        double minAngleForCurrentButton = buttonInfoArray[currentButtonIndex].minAngle;
        double targetAngleForCurrentButton;

        if (effectiveDelta.y > 0) {
            targetAngleForCurrentButton = maxAngleForCurrentButton;
        } else {
            targetAngleForCurrentButton = minAngleForCurrentButton;
        }
        if (percentageCompleted == -1) {
            boolean isYDisplacement = effectiveDelta.y > effectiveDelta.x ? true
                    : false;

            isYDisplacement = true;
            if (isYDisplacement) {
                float newY = buttonInfoArray[currentButtonIndex].OrigPos.y
                        + effectiveDelta.y;

                if (newY > (centerPoint.y) + (int) radius) {
                    newY = (centerPoint.y) + (int) radius;
                } else if (newY < (centerPoint.y) - (int) radius) {
                    newY = (centerPoint.y) - (int) radius;
                }
                float newX = getXPos(newY);

                newPos = new PointF(newX, newY);
                s1.append("isYDisplacement true : ");

            }

        } else {

            double effectiveAngle = buttonInfoArray[currentButtonIndex].origAngle
                    + ((targetAngleForCurrentButton - buttonInfoArray[currentButtonIndex].origAngle) * percentageCompleted);

            newPos = getPointByAngle(effectiveAngle);

            s1.append("percentage completed : " + percentageCompleted + " : "
                    + effectiveAngle);
        }

        double newAngle = getNewAngle(newPos);

        // For angle, reverse condition, because in 1st quarter, it is +ve, in
        // 4th quarter, it is -ve.

        if (newAngle < maxAngleForCurrentButton) {
            newAngle = maxAngleForCurrentButton;
            newPos = getPointByAngle(newAngle);
            s1.append("max angle : " + newAngle);
        }

        if (newAngle > minAngleForCurrentButton) {
            newAngle = minAngleForCurrentButton;
            newPos = getPointByAngle(newAngle);
            s1.append("min angle : " + newAngle);
        }

        setTranslationX(buttonInfoArray[currentButtonIndex].btnObj,
                newPos.x - 50);
        setTranslationY(buttonInfoArray[currentButtonIndex].btnObj,
                newPos.y - 50);

        return newAngle;

    }

    /**
     * Set button Position
     * @param deltaPoint
     */
    public void updateButtonPos(PointF deltaPoint) {

        for (int buttonIndex = 0; buttonIndex < NUM_BUTTONS; buttonIndex++) {
            if (currentButton == buttonIndex) {

                buttonInfoArray[buttonIndex].currentAngle = updateControl(
                        buttonIndex, deltaPoint, -1);

                double targetAngleForCurrentButton;

                if (deltaPoint.y > 0) {
                    targetAngleForCurrentButton = buttonInfoArray[buttonIndex].maxAngle;
                } else {
                    targetAngleForCurrentButton = buttonInfoArray[buttonIndex].minAngle;
                }

                double percentageCompleted = (1.0 * (buttonInfoArray[buttonIndex].currentAngle - buttonInfoArray[buttonIndex].origAngle))
                        / (targetAngleForCurrentButton - buttonInfoArray[buttonIndex].origAngle);

                for (int innerButtonIndex = 0; innerButtonIndex < NUM_BUTTONS; innerButtonIndex++) {
                    if (innerButtonIndex == buttonIndex)
                        continue;
                    buttonInfoArray[innerButtonIndex].currentAngle = updateControl(
                            innerButtonIndex, deltaPoint, percentageCompleted);
                }
                break;
            }
        }
    }

    /**
     * Find whether touch in button's rectanlge or not
     * @param v
     * @param rect
     */
    private static void getHitRect(View v, Rect rect) {
        rect.left = (int) com.nineoldandroids.view.ViewHelper.getX(v);
        rect.top = (int) com.nineoldandroids.view.ViewHelper.getY(v);
        rect.right = rect.left + v.getWidth();
        rect.bottom = rect.top + v.getHeight();
    }

    private boolean isRectHit(View viewObj, MotionEvent motionEvent,
            PointF viewOrigPos) {
        Rect outRect = new Rect();


        int x = (int) motionEvent.getX();
        int y = (int) motionEvent.getY();

        getHitRect(viewObj, outRect);

        if (outRect.contains(x, y)) {
            return true;
        } else {
            return false;
        }

    }

    /**
     * On Finish update transition
     */
    @Override
    public void finish() {
        super.finish();
        overridePendingTransition(R.anim.activityfinishin, R.anim.activityfinishout);
    }

    /**
     * On Native Back Pressed
     */
    @Override
    public void onBackPressed() {
        super.onBackPressed();
        finish();
    }   

}