Search code examples
androiddrag

How do I implement onDragListener in an object within a fragment?


My app has a class, MarkedLine, which extends View. An instance of this class is shown in a Fragment. I want users to be able to do the following 3 things:

  1. Enlarge the line by doing "pinch" and "stretch" gestures
  2. Touch any point of the line and get its coordinates
  3. Move the line around

I have the first two working, but can't figure out the third one (the dragging).

Each MarkedLine consists of a horizontal line of boxes, some of which are coloured in. The user can zoom in by stretching, and tap a box to change its colour; I also want them to be able to move the line around the screen, because when it's zoomed in it will go off the edges of the screen.

The basic fragment layout (fragment_marked_line) is as follows (I've removed irrelevant bits, padding, margins etc.):

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout   
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:res="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <packagepath.models.ToolboxButton
        android:id="@+id/toolbarNext"
        android:layout_width="@dimen/toolbar_icon_size"
        android:layout_height="@dimen/toolbar_icon_size"
        android:src="@drawable/next_line"
        res:layout_constraintTop_toTopOf="parent" />

    <packagepath.models.MarkedLine
        android:id="@+id/markedLine"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        res:layout_constraintStart_toStartOf="parent"
        res:layout_constraintEnd_toEndOf="parent"
        res:layout_constraintTop_toBottomOf="@+id/toolbarNext" />

</android.support.constraint.ConstraintLayout>

(So basically it's a button with a full-width line underneath it. The button allows the user to bring up the next line).

The Fragment code (MarkedLineFragment) is as follows (n.b. A LineSet is basically just an array of MarkedLines, with a few extra variables like when it was created, the line dimensions etc):

public class MarkedLineFragment extends Fragment {

    LineSet       mLineSet
    MarkedLine    mMarkedLine;
    ToolboxButton btn_next;

    int     mItemNumber, mMaxItems;

    public MarkedLineFragment() {}

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                     ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);

        View rootView = inflater.inflate(
                R.layout.fragment_marked_line, container, false);

        // Get view objects
        btn_next = rootView.findViewById(R.id.toolbarNext);
        mMarkedLine = rootView.findViewById(R.id.markedLine);

        // Initialise the button
        initialise_button();

        // If the LineSet has already been set, 
        //  pass it through to the MarkedLine
        if(mLineSet != null) {
            mMarkedLine.setLineSet(mLineSet);
            mMaxItems = mLineSet.getNum_items();
        }

        // Initialise at line 1
        mItemNumber = 1;
        mMarkedLine.setCurrentItem(mItemNumber);

        // Draw the MarkedLine
        drawLine();

        return rootView;
    }

    // Initialise the button so that it moves to the next line on clicking
    public void initialise_button() {

        btn_next.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(mItemNumber == mMaxItems) return;
                else mItemNumber += 1;
                set_new_item_number();
            }
        });

    }

    private void set_new_item_number() {
        mMarkedLine.setCurrentItem(mItemNumber);
    }

    public void drawChart() {
      if(mMarkedLine != null) mMarkedLine.postInvalidate();
    }

}

Finally, MarkedLine class (I've left out the details of how the line is drawn, because I don't think it's relevant, and it's quite long - but I can add it in if needed):

public class MarkedLine extends View {

    private LineSet mLineSet;
    private int currentItem;
    private int numBoxes;
    private float canvas_height, canvas_width;
    private float box_size;
    private float minX, maxX, minY, maxY;

    // Scaling (pinch & zoom) variables
    private float scaleFactor = 1.0f; // Current scale factor
    private ScaleGestureDetector detectorScale;// Detector for gestures
    private GestureDetector detectorTouch; // Detector for tap gestures

    public MarkedLine(Context thisContext, AttributeSet attrs) {
        super(thisContext, attrs);

        detectorScale = new ScaleGestureDetector(thisContext, new MarkedLine.ScaleListener());
        detectorTouch = new GestureDetector(thisContext, new MarkedLine.TouchListener());

    }

    public void setCallback(OnBoxTouched callback) { mCallback = callback; }

    public void setLineSet(LineSet lineSet) {
        mLineSet = lineSet;
        numBoxes = mLineSet.getNum_boxes();
        invalidate();
    }

    public void setCurrentItem(int newItemNumber) {
        currentItem = newItemNumber;
        invalidate();
    }

    protected void onDraw(Canvas canvas) {

        if (mLineSet == null) return;

        // Set up canvas
        canvas.save();
        canvas.scale(scaleFactor, scaleFactor);
        canvas.translate(translateX / scaleFactor, translateY / scaleFactor);

        // draw_boxes reads how many boxes make up the MarkedLine,
        //  calculates what size they need to be to fit on the canvas, 
        //  and then draws them
        draw_boxes();

        // fill_in_line adds in the appropriate colours to the 
        //  boxes in the line
        fill_in_line();

        canvas.restore();
    }

    // GRID EVENT FUNCTIONS - respond to User touching screen

    // onTouchEvent
    // User has touched the screen - trigger listeners
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        detectorScale.onTouchEvent(event);
        detectorTouch.onTouchEvent(event);
        invalidate();

        return true;

    }

    // LISTENERS

    /*
     * Respond to user touching the screen
     */
    private class TouchListener extends GestureDetector.SimpleOnGestureListener {

        public boolean onSingleTapUp(MotionEvent event) {

            // Determine where the screen was touched
            float xTouch = event.getX();
            float yTouch = event.getY();

            // Check that the touch was within the line; return if not
            if(!touch_in_line(xTouch, yTouch)) return false;

            // Figure out which Box was tapped
            int xCell = getTouchedBox(xTouch);

            // Now the box which was tapped is coloured in
            colour_box(xCell);

            return true;
        }

    }

    /*
     * Determine scale factor for zoom mode
     * This can be called in View and Edit Activities
     */
    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

        @Override
        public boolean onScale(ScaleGestureDetector detector) {

            float MIN_ZOOM = 1f;     // Minimum zoom level
            float MAX_ZOOM = 5f;     // Maximum zoom level

            scaleFactor *= detector.getScaleFactor();
            scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
            return true;
        }

    }
}

This all works fine. The user can stretch the line to make the boxes bigger/smaller, and then tap on any of the boxes on the line to colour them in. However, I can't get the box to move around the screen when the user drags their finger on it.

I assume I need to add an onDragListener to something, but I can't figure out what. I tried having a DragListener class, similar to the ScaleListener and TouchListener classes, with an onDrag method (I just has a couple of dummy lines so I could attach a breakpoint). Then I declared an instance of that class (dragListener). I tried attaching it in the MarkedLine constructor using this.onDragListener(dragListener) but it didn't respond to dragging.

Then I attempted something similar in the Fragment, attaching it to the mMarkedLine class in onCreateView, but again it didn't respond when I tried to drag.

I've read the Android documentation, which suggested the onDragListener class, but I'm clearly doing something wrong.


Solution

  • I fixed this by adding a check for dragging in the onTouchEvent of the MarkedLine class:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    
        detectorScale.onTouchEvent(event);
        detectorTouch.onTouchEvent(event);
    
        // Check for drag gestures
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = event.getX() - previousTranslateX;
                startY = event.getY() - previousTranslateY;
                break;
            case MotionEvent.ACTION_UP:
                previousTranslateX = translateX;
                previousTranslateY = translateY;
                break;
            case MotionEvent.ACTION_MOVE:
                translateX = event.getX() - startX;
                translateY = event.getY() - startY;
                break;
        }
    
        invalidate();
    
        return true;
    
    }