2015-04-23 10 views
9

Versione corta:Come rendere gli elementi della lista ottengono DragEvents dopo lo scorrimento

  • Esiste un modo per fare una nuova visualizzazione creata ricevere DragEvent s di un'operazione di drag-and-drop già in esecuzione?

C'è How to register a DragEvent while already inside one and have it listen in the current DragEvent?, ma mi piacerebbe davvero una soluzione più pulita.

La soluzione GONE-> VISIBLE suggerita è abbastanza complessa da ottenere "correttamente", perché è necessario assicurarsi di usarla solo quando un elemento della lista diventa visibile e non incondizionatamente su tutte le voci della vista elenco corrente. In questo l'hack è leggermente perdente senza nemmeno più codice risolvibile per farlo bene.

Versione lunga:

Ho un ListView. Gli elementi di ListView sono View personalizzati che contengono simboli trascinabili (piccole caselle), ad es. simile a questo:

Example playout

È possibile trascinare le piccole scatole tra le voci del ListView, come l'ordinamento elementi in scatole. Il gestore di trascinamento sulle voci di elenco è più o meno banale:

@Override 
public boolean onDragEvent(DragEvent event) 
{ 
    if ((event.getLocalState() instanceof DragableSymbolView)) { 
     final DragableSymbolView draggedView = (DragableSymbolView) event.getLocalState(); 
     if (draggedView.getTag() instanceof SymbolData) { 
      final SymbolData symbol = (SymbolData) draggedView.getTag(); 
      switch (event.getAction()) { 
      case DragEvent.ACTION_DRAG_STARTED: 
       return true; 

      case DragEvent.ACTION_DRAG_ENTERED: 
       setSelected(true); 
       return true; 

      case DragEvent.ACTION_DRAG_ENDED: 
      case DragEvent.ACTION_DRAG_EXITED: 
       setSelected(false); 
       return true; 

      case DragEvent.ACTION_DROP: 
       setSelected(false); 
       // [...] remove symbol from soruce box and add to current box 
       requestFocus(); 
       break; 
      } 
     } 
    } 

    return super.onDragEvent(event); 
} 

Il trascinamento inizia quando si tiene il puntatore su un simbolo e iniziando a trascinare (vale a dire lo spostamento al di là di una piccola soglia).

Ora, tuttavia, le dimensioni dello schermo potrebbero non essere sufficienti per contenere tutte le caselle e quindi lo ListView deve scorrere. Ho scoperto il modo in cui devo implementare lo scorrimento da solo, dal momento che lo ListView non scorre automaticamente durante il trascinamento.

In arriva ListViewScrollingDragListener:

public class ListViewScrollingDragListener 
    implements View.OnDragListener { 

    private final ListView _listView; 

    public static final int DEFAULT_SCROLL_BUFFER_DIP = 96; 
    public static final int DEFAULT_SCROLL_DELTA_UP_DIP = 48; 
    public static final int DEFAULT_SCROLL_DELTA_DOWN_DIP = 48; 

    private int _scrollDeltaUp; 
    private int _scrollDeltaDown; 

    private boolean _doScroll = false; 
    private boolean _scrollActive = false; 

    private int _scrollDelta = 0; 

    private int _scrollDelay = 250; 
    private int _scrollInterval = 100; 

    private int _scrollBuffer; 

    private final Rect _visibleRect = new Rect(); 

    private final Runnable _scrollHandler = new Runnable() { 

     @Override 
     public void run() 
     { 
      if (_doScroll && (_scrollDelta != 0) && _listView.canScrollVertically(_scrollDelta)) { 
       _scrollActive = true; 
       _listView.smoothScrollBy(_scrollDelta, _scrollInterval); 
       _listView.postDelayed(this, _scrollInterval); 
      } else { 
       _scrollActive = false; 
      } 
     } 
    }; 

    public ListViewScrollingDragListener(final ListView listView, final boolean attach) 
    { 
     _scrollBuffer = UnitUtil.dipToPixels(listView, DEFAULT_SCROLL_BUFFER_DIP); 
     _scrollDeltaUp = -UnitUtil.dipToPixels(listView, DEFAULT_SCROLL_DELTA_UP_DIP); 
     _scrollDeltaDown = UnitUtil.dipToPixels(listView, DEFAULT_SCROLL_DELTA_DOWN_DIP); 

     _listView = listView; 
     if (attach) { 
      _listView.setOnDragListener(this); 
     } 
    } 

    public ListViewScrollingDragListener(final ListView listView) 
    { 
     this(listView, true); 
    } 

    protected void handleDragLocation(final float x, final float y) 
    { 
     _listView.getGlobalVisibleRect(_visibleRect); 
     if (_visibleRect.contains((int) x, (int) y)) { 
      if (y < _visibleRect.top + _scrollBuffer) { 
       _scrollDelta = _scrollDeltaUp; 
       _doScroll = true; 
      } else if (y > _visibleRect.bottom - _scrollBuffer) { 
       _scrollDelta = _scrollDeltaDown; 
       _doScroll = true; 
      } else { 
       _doScroll = false; 
       _scrollDelta = 0; 
      } 
      if ((_doScroll) && (!_scrollActive)) { 
       _scrollActive = true; 
       _listView.postDelayed(_scrollHandler, _scrollDelay); 
      } 
     } 
    } 

    public ListView getListView() 
    { 
     return _listView; 
    } 

    @Override 
    public boolean onDrag(View v, DragEvent event) 
    { 
     /* hide sequence controls during drag */ 
     switch (event.getAction()) { 
     case DragEvent.ACTION_DRAG_ENTERED: 
      _doScroll = true; 
      break; 

     case DragEvent.ACTION_DRAG_EXITED: 
     case DragEvent.ACTION_DRAG_ENDED: 
     case DragEvent.ACTION_DROP: 
      _doScroll = false; 
      break; 

     case DragEvent.ACTION_DRAG_LOCATION: 
      handleDragLocation(event.getX(), event.getY()); 
      break; 
     } 
     return true; 
    } 
} 

Questo Scorre fondamentalmente la ListView quando si trascina vicino al confine superiore o inferiore della sua area visibile. Non è perfetto, ma è abbastanza buono.

Tuttavia, c'è un problema:

Quando l'elenco scorre ad un elemento di prima invisibili, quell'elemento non riceve DragEvent s. Non viene selezionato (evidenziato) quando si trascina un simbolo su di esso e non accetta gocce.

Qualsiasi idea su come rendere le viste "a scorrimento" riceve DragEvent s dall'operazione di trascinamento della selezione già attiva?

risposta

3

Quindi il problema fondamentale è che ViewGroup (che ListView estende) memorizza nella cache una lista di bambini da notificare a DragEvent. Inoltre, popola questa cache solo quando riceve ACTION_DRAG_STARTED. Per ulteriori dettagli, consultare il codice sorgente here.

Sulla soluzione!Invece di ascoltare gli eventi di rilascio sulle singole righe di ListView, li ascolteremo su ListView stesso. Quindi, in base alle coordinate degli eventi, scopriremo da quale riga viene trascinata la vista trascinata da/verso o al passaggio del mouse. Quando si verifica il rilascio, eseguiremo la transazione di rimozione dalla riga precedente e aggiunta alla nuova riga.

private void init(Context context) { 
    setAdapter(new RandomIconAdapter()); // Adapter that contains our data set 
    setOnDragListener(new ListDragListener()); 
    mListViewScrollingDragListener = new ListViewScrollingDragListener(this, false); 
} 

ListViewScrollingDragListener mListViewScrollingDragListener; 

private class ListDragListener implements OnDragListener { 
    // The view that our dragged view would be dropped on 
    private View mCurrentDropZoneView = null; 
    private int mDropStartRowIndex = -1; 

    @Override 
    public boolean onDrag(View v, DragEvent event) { 
     switch (event.getAction()) { 
      case DragEvent.ACTION_DRAG_LOCATION: 
       // Update the active drop zone based on the position of the event 
       updateCurrentDropZoneView(event); 

       // Funnel drag events to separate listener to handle scrolling near edges 
       mListViewScrollingDragListener.onDrag(v, event); 

       if(mDropStartRowIndex == -1)   // Only initialize once per drag->drop gesture 
       { 
        mDropStartRowIndex = indexOfChild(mCurrentDropZoneView) + getFirstVisiblePosition(); 
        log("mDropStartRowIndex %d", mDropStartRowIndex); 
       } 
       break; 
      case DragEvent.ACTION_DRAG_ENDED: 
      case DragEvent.ACTION_DRAG_EXITED: 
       mCurrentDropZoneView = null; 
       mDropStartRowIndex = -1; 
       break; 
      case DragEvent.ACTION_DROP: 
       // Update our data set based on the row that the dragged view was dropped in 
       int finalDropRow = indexOfChild(mCurrentDropZoneView) + getFirstVisiblePosition(); 
       updateDataSetWithDrop(mDropStartRowIndex, finalDropRow); 

       // Let adapter update ui 
       ((BaseAdapter)getAdapter()).notifyDataSetChanged(); 

       break; 
     } 

     // The ListView handles ALL drag events all the time. Fine for now since we don't need to 
     // drag -> drop outside of the ListView. 
     return true; 
    } 

    private void updateDataSetWithDrop(int fromRow, int toRow) { 
     log("updateDataSetWithDrop fromRow %d and toRow %d", fromRow, toRow); 
     sIconsForListItems[fromRow]--; 
     sIconsForListItems[toRow]++; 
    } 

    // NOTE: The DragEvent in local to DragDropListView, as are children coordinates 
    private void updateCurrentDropZoneView(DragEvent event) { 
     View previousDropZoneView = mCurrentDropZoneView; 
     mCurrentDropZoneView = findFrontmostDroppableChildAt(event.getX(), event.getY()); 
     log("mCurrentDropZoneView updated to %d for x/y : %f/%f with action %d", 
       mCurrentDropZoneView == null ? -1 : indexOfChild(mCurrentDropZoneView) + getFirstVisiblePosition(), 
       event.getX(), event.getY(), event.getAction()); 

     if (mCurrentDropZoneView != previousDropZoneView) { 
      if (previousDropZoneView != null) previousDropZoneView.setSelected(false); 
      if (mCurrentDropZoneView != null) mCurrentDropZoneView.setSelected(true); 
     } 
    } 
} 

/** 
* The next four methods are utility methods taken from Android Source Code. Most are package-private on View 
* or ViewGroup so I'm forced to replicate them here. Original source can be found: 
* http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.0_r1/android/view/ViewGroup.java#ViewGroup.findFrontmostDroppableChildAt%28float%2Cfloat%2Candroid.graphics.PointF%29 
*/ 
private View findFrontmostDroppableChildAt(float x, float y) { 
    int childCount = this.getChildCount(); 
    for(int i=0; i<childCount; i++) 
    { 
     View child = getChildAt(i); 
     if (isTransformedTouchPointInView(x, y, child)) { 
      return child; 
     } 
    } 

    return null; 
} 

static public boolean isTransformedTouchPointInView(float x, float y, View child) { 
    PointF point = new PointF(x, y); 
    transformPointToViewLocal(point, child); 
    return pointInView(child, point.x, point.y); 
} 

static public void transformPointToViewLocal(PointF pointToModify, View child) { 
    pointToModify.x -= child.getLeft(); 
    pointToModify.y -= child.getTop(); 
} 

static public boolean pointInView(View v, float localX, float localY) { 
    return localX >= 0 && localX < (v.getRight() - v.getLeft()) 
       && localY >= 0 && localY < (v.getBottom() - v.getTop()); 
} 

static final int[] sIconsForListItems; 
static final int NUM_LIST_ITEMS = 50; 
static final int MAX_NUM_ICON_PER_ELEMENT = 8; 
static { 
    sIconsForListItems = new int[NUM_LIST_ITEMS]; 
    for (int i=0; i < NUM_LIST_ITEMS; i++) 
    { 
     sIconsForListItems[i] = (getRand(MAX_NUM_ICON_PER_ELEMENT)); 
    } 
} 

private static final String TAG = DragDropListView.class.getSimpleName(); 
private static void log(String format, Object... args) { 
    Log.d(TAG, String.format(format, args)); 
} 

Un sacco di commenti, quindi si spera che il codice sia auto-documentante. Alcune note:

  • RandomIconAdapter è solo un adattatore di base che estende BaseAdapter e supportato da sIconsForListItems.
  • ListViewScrollingDragListener è uguale a quello nel prompt.
  • Testato su GS6 5.0.2
+0

Mi ha salvato un sacco di tempo, grazie! –

Problemi correlati