Tap and Drag Animation Domination Part 3 of 3: Implementing Drag and Drop in Sencha Touch 2

In this final blog article, we’ll look at how to implement the animated tap and drag ui depicted below. If you haven’t already done so, please read the following related posts:

The tap and drag interface is comprised of the following two custom classes:

  • DD.view.DraggableItem : An Ext.Container with custom styling to implement rounded edges and box shadow as well as configurable label and id properties
  • DD.view.DDEditor : An Ext.Container that instantiates the draggable items, sets up the drop grid, and handles drag and drop activities.

Reviewing the DragableItem Class

The DraggableItem class is fairly straightforward – it’s simply a themed container with a toolbar that could potentially have buttons added to it for use in some other context.

Ext.define('DD.view.DraggableItem', {
 extend: 'Ext.Container',
 alias: 'widget.draggablesetting',

 requires: ['Ext.Label'],

 config: {
  label: '',       // configurable text label
  sectionId: 0,    // pass in a unique identifier
  cls: 'draggablesetting', // unique dom selector
  initialIndex: 0,
  
  // configure physical characteristics
  // of the draggable
  height: 60,
  margin: '0 10 0 10',
  style: 'border-radius: 5px; background-color: lightgray; opacity: 0.0; box-shadow: 0px 0px 10px rgba(50, 50, 50, 0.75);',
  width: 380,
  
  // subcomponents      
  items: [
   {
    xtype: 'container',
    docked: 'top',
    height: 50,
    itemId: 'toolbar',
    layout: {
     type: 'hbox'
    },
    items: [
     {
      xtype: 'label',
      flex: 1,
      height: 50,
      margin: '5 5 0 5'
     }
    ]
   } 
  ] // draggable subitems
 },

 initialize: function() {
  this.callParent(arguments);
  // set the label on the draggable
  this.down('label').setHtml(this.getLabel());
 }
});

Reviewing the Drag and Drop Container

The DDEditor class is a lot more complicated as it implements all of the drag and drop logic and animations used in the example.

The class starts out simply enough by extending the Ext.Container class and docking the titlebar, instructions text, and button bar at the top and bottom of the panel.

Note the following:

  • The slots property is an array that tracks the current sorted order of the draggable items
  • The save button handler throws an event that passes an array containing the label/id properties of the draggables (in the order in which the user sorted them) up to a controller.
  • The Ext.Anim class is required in order to add the groovy animations, discussed later in this post.
  • The entire body of the container, minus the area occupied by the docked components, becomes the drag/drop zone.
Ext.define('DD.view.DDEditor', {
 extend: 'Ext.Container',
 alias: 'widget.arranger',
 requires: ['Ext.Anim', 'Ext.TitleBar'],
 
// private properties
 slots : [], // an array representing the item present in 
             // each drop area
 slot : {},
 
 // 80 pixels per "drop slot"
 spacing : 80,

 config: {
  centered: true,
  height: 540,
  width: 400,
  style: 'background-color: white; webkit-box-shadow: 0px 3px 14px rgba(51, 50, 50, 0.65)',
  title: "Section Editor",
  layout: { type: 'fit'},
  modal: true,
  hideOnMaskTap: true,      
  hideAnimation: {
   type: 'fadeOut',
   duration: 200
  },
  
  // draggables for the window      
  dragItems: [
          { id: 1, label: 'Section One' },
          { id: 2, label: 'Section Two' },
          { id: 3, label: 'Section Three' },
          { id: 4, label: 'Section Four' }
  ],

  items: [
   {
    xtype: 'toolbar',
    docked: 'bottom',
    itemId: 'toolbar',
    layout: { pack: 'end', type: 'hbox'},
    items: [
      {
       xtype: 'button',
       left: 5,
       top: 6,
       text: 'Reset',
       handler: function(button, event) {
         var top = button.up('arranger');
         top.resetSettings.call(top); // described later
       }
      },
      {
       xtype: 'button',
       ui: 'confirm',
       text: 'Save',
       handler: function(button, event) {
         var top = button.up('arranger');
         var cmp = {};
         var results = [];
         for (var i=0; i<top.slots.length; i++) {
           cmp = Ext.getCmp(top.slots[i].id);
           results.push({
             id: cmp.getSectionId(),
             label: cmp.getLabel()
           });
          }
          console.log('order of selections: ', results);
          top.fireEvent('save',top,results);
       }
      }
     ]
    },
    {
      xtype: 'titlebar',
      docked: 'top'
    },
    {
      xtype: 'component',
      docked: 'top',
      height: 70,
      html: '<p style="font-size: 0.8em">Tap and drag fields to customize your view. Tap the Save button to save your preferences. Tap Reset to revert to the previous layout.</p>',
      itemId: 'instructions',
      margin: '5 5 20 8'
     }
   ],
   listeners: [
    {
     fn: 'onContainerHide',
     event: 'hide'
    }
   ]
 },

 onContainerHide: function(component, eOpts) {
  component.destroy();
 }
});

Instantiating the Draggables

The initialize() method of the component sets the title of the titlebar and instantiates the draggable items into the ui.

initialize: function() {
 
 this.callParent(arguments);
 
 // set the text on the titlebar
 this.down('titlebar').setTitle(this.getTitle());

 // define draggable sections
 var layoutItems = [];
 
 var sections = this.getDragItems();
 this.numItems = sections.length; // cache num items for spacing calculation
 var draggable = {};

 // build array of draggables

 for (var i=0; i<sections.length; i++) {
   draggable = {
     xtype: 'draggablesetting',
     hidden: false,
     initialIndex: i,
     draggable: {
       direction: 'vertical',
       listeners: {
        drag: this.onDrag,
        dragend: this.onDrop,
        dragstart: this.onDragStart,
        scope: this
       }
     },
     itemId: 'draggable' + i,
     label: sections[i].label,
     sectionId: sections[i].id
   }; // draggable 
   layoutItems.push(draggable);
 }
 
 // add draggables to the container
 this.add(layoutItems);
 
 // a little bit of a simple kludge here - 
 // delay initializing draggables until they've been 
 // fully instantiated.
 Ext.Function.defer(this.initializeDraggables,250,this);
}

Implementing the initial fade-in animation

The initializeDraggables() method performs the following two functions:

  • It fills the slots array with pointers to the top-level dom elements that were created by instantiating the ‘draggablesetting’ components
  • It animates the move of each draggable into its initial associated drop slot
initializeDraggables: function() {
 
 var me = this;
 this.slots = [];
 
 // get pointers to the top-level dom elements
 // created by each draggable component

 var els = Ext.select("div.draggablesetting");
 
 // loop through the draggables and animate their move
 // to an initial position
    
 for (var i=0; i<els.elements.length; i++) {

   var el = Ext.get(els.elements[i]);
   
   // calculate each draggable initial, vertical position
   var tf = 'translate3d(0px, ' + (( i * this.spacing) + 1) + 'px,0px)';

   // cache a pointer to the draggable dom element
   this.slots[i] = el;

   // run the initial fade/move animation
   // note that animations run asynchronously

   Ext.Anim.run(el, new Ext.Anim({
     autoClear: true,
     easing: 'ease-in',
     duration: 500,
     from: {
      '-webkit-transform': 'translate3d(0px,0px,0px)',
      'opacity': 0.0
     },
     to: {
      '-webkit-transform': tf,
      'opacity' : 1.0
     },
     after: function(el) {
      // after animation is complete, formally
      // move component into initial position

      // get pointer to related Ext.Component
      var cmp =  Ext.getCmp(el.getId());

      // position the initial drag position of the component
      cmp.getDraggable().setOffset(0,cmp.getInitialIndex() * me.spacing);
     }
  }));
  }
}

Handling Drag Actions

The following three handlers take care of the drag action:

  • onDragStart : Determines the slot in which the draggable is currently sitting
  • onDrag : Runs animations on the draggables that are being encroached upon by the item currently being dragged.
  • onDrop: Locks the draggable components into their new positions
onDragStart: function(el, e, offsetX, offsetY, eOpts) {
 this.slot = Ext.Number.toFixed(offsetY / this.spacing,0);
},
    
onDrag: function(dd, e, offsetX, offsetY, eOpts) {
 el = dd.getElement();

 // collision detection on slot
 var oldslot = this.slot;
 var slot = Ext.Number.constrain(Ext.Number.toFixed(offsetY / this.spacing,0),0,this.slots.length);

 if (slot != this.slot && slot >= 0) {
  this.slot = slot;

  // move item in new slot out of the way
  var tf1 = 'translate3d(0px, ' + ( this.slot * this.spacing) + 'px,0px)';
  var tf2 = 'translate3d(0px, ' + ( oldslot * this.spacing) + 'px,0px)';

  Ext.Anim.run(this.slots[this.slot], new Ext.Anim({
                autoClear: false,
                easing: 'ease-in',
                duration: 500,
                from: {
                    '-webkit-transform': tf1
                },
                to: {
                    '-webkit-transform': tf2
                }
   }));

   var temp = this.slots[this.slot];
   this.slots[this.slot] = el;
   this.slots[oldslot] = temp;
 }
},

onDrop: function(el, e, offsetX, offsetY, eOpts) {
 
 // snap all draggables into their "slots"
 for (var i=0; i<this.slots.length; i++) {
   var cmp =  Ext.getCmp(this.slots[i].getId());
   cmp.getDraggable().setOffset(0,i * this.spacing);
  }
}

Resetting the draggables back to their initial positions

The resetSettings() method, illustrated below, applies an animation to each of the draggables, causing them to float back up to the top of the panel.

Once the animation is complete (after 400 ms), the draggable components are all destroyed and the component’s initialize() method is re-invoked, thereby starting the process anew.

resetSettings: function(settings) {
  // roll back up
  for (var i=0; i<this.slots.length; i++) {
    Ext.Anim.run(this.slots[i], new Ext.Anim({
      autoClear: false,
      easing: 'ease-in',
      duration: 400,
      from: {
        '-webkit-transform': this.slots[i].getStyle('-webkit-transform')
      },
      to: {
        '-webkit-transform': 'translate3d(0px,0px,0px)' 
      }
    }));
   }

   Ext.Function.defer(function() {
    var draggables = this.query('draggablesetting');
    for (var i=0; i<draggables.length; i++) {
       draggables[i].destroy();   
    }       
    this.initialize();
   },400,this);
}

Run the Example and Download the Code!

7 thoughts on “Tap and Drag Animation Domination Part 3 of 3: Implementing Drag and Drop in Sencha Touch 2

  1. Ayush Verma

    Sir nice tutorial but sir i need drag n drop of images in horizontal n vertical direction and if i select image and shift it to right then it will allow scroll active on right as well as vertical direction as well as swapping of images like u shown in this example

    Reply
    1. sdrucker Post author

      I’ve posted a production build of the demo app, which cuts the number of http requests significantly. This should address your “failed loading” errors.

      Reply
  2. anilvardhan

    Hi sdrucker, it’s really useful one.
    I used the same in my project…working perfect.

    But i need the same drag should start after TapHold/LongPress”, could you please share some code snippet where i can change/add in this code.

    Advance thanks.

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s