Multi-Window Simple Examples: Part 2 – Drag and Drop
Introduction
Drag and drop are a little bit more difficult than handling screen changes (Part 1). Why is that? Mainly because we don’t want to just trigger the drop action to another application but also to send something (i.e. some text, resource or something else).
This time, I’ll present a simple example of how to handle drag and drop without worrying about permissions for now. For this reason, I’ve created two applications.
Catch’em all!
The first application, MultiWindowCatch, you should know from Part 1. You can find the app on the bottom of this article.
This app simply shows first nine Pokémons – each evolution of bulbasaur, charmander and squirtle. And that’s it. If we want to catch more Pokémons, first of all, we have to install the MultiWindowDropAndAdjacent app.
After that we have to make only 5 moves to get new Pokémon:
- Click on pokéball icon on the toolbar.
- Long click on Overview button.
- Select MultiWindowDropAndAdjacent app with other Pokémons (now we should see two apps at once).
- By clicking left and right arrow chose which Pokémon we want to catch.
- Drag pokéball and drop it on chosen Pokémon.
Start drag and drop
In PokeballActivity we want to start drag and drop by long press on pokéball image.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public class PokeballActivity extends AppCompatActivity { @BindView(R.id.image_pokeball) ImageView mImagePokeball; //... @OnLongClick(R.id.image_pokeball) public boolean onPokeballLongClick() { if (isAndroidNOrLater()) { startPokeballDragAndDrop(); } return true; } private boolean isAndroidNOrLater() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; } @SuppressLint("NewApi") private void startPokeballDragAndDrop() { mImagePokeball.startDragAndDrop( ClipDataCreator.getClipData(), new View.DragShadowBuilder(mImagePokeball), null, View.DRAG_FLAG_GLOBAL); } } |
As you can see I use ButterKnife (@BindView, @OnLongClick) – it is very handy.
Firstly I check if we have at least Android N and then I run View.startDragAndDrop() method. We have to pass a couple of parameters to this method:
1. ClipData – an object with data that we pass to other application. This should be a simple example, right? So we should create that object in a very basic way.
1 2 3 4 5 6 7 8 9 | public class ClipDataCreator { private static final String POKEBALL = "POKEBALL"; public static ClipData getClipData() { ClipData.Item item = new ClipData.Item(POKEBALL); return new ClipData(POKEBALL, new String[]{}, item); } } |
We just pass the ClipData.An item with text “POKEBALL” and base on this in other application (MultiWindowDropAndAdjacent) we will be able to know that some application wants to catch current Pokémon on the screen.
2. View.DragShadowBuilder – an object that will draw a shadow during dragging.
3. Local data Object – we don’t need this.
4. Flag View.DRAG_FLAG_GLOBAL. The documentation says “To enable cross-activity drag and drop, pass the new flag View.DRAG_FLAG_GLOBAL
“, so I passed it (and as I said, in this example, we don’t worry about permissions).
Basically View.startDragAndDrop() method is a new implementation for View.startDrag() (from N is deprecated). You can read more about drag and drop process in Drag and Drop chapter in API Guides. Implementation of View.startDrag() method you will find in section Designing a Drag and Drop Operation.
Drop in different app
Ok, so now let’s move from MultiWindowCatch app to MultiWindowDropAndAdjacent and drop the pokéball.
What we want to do is to drop pokéball on selected new pokéball and trigger that action, so we have to implement drag listener on Pokémon image on which we will drop pokéball.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class MainActivity extends AppCompatActivity implements MainView { private MainPresenter mPresenter; @Override protected void onCreate(Bundle savedInstanceState) { //... mPresenter = new MainPresenter(); mImagePokemon.setOnDragListener((view, dragEvent) -> sendPokemonIdIfDrop(dragEvent)); } private boolean sendPokemonImageUrlIfDrop(final DragEvent dragEvent) { if (isDropAction(dragEvent)) { mPresenter.pokeballDropped((dragEvent.getClipData().getItemAt(0)).getText().toString()); } return true; } private boolean isDropAction(final DragEvent dragEvent) { return dragEvent.getAction() == DragEvent.ACTION_DROP; } } |
I used retrolambda in listener, but if you don’t like it, here you have:
1 2 3 4 5 6 | mImagePokemon.setOnDragListener(new View.OnDragListener() { @Override public boolean onDrag(final View view, final DragEvent dragEvent) { return sendPokemonIdIfDrop(dragEvent); } }); |
I pass DragEvent do sendPokemonIdIfDrop() method and simply check if user dropped something. We can also listen for more events than DragEvent.ACTION_DROP like DragEvent.ACTION_DRAG_STARTED or DragEvent.ACTION_DRAG_ENDED. The most interesting part is when we start dragging something in another app, as all the drag events reach also to our listener in our app.
Then I get ClipData.Item from ClipData and paste the text from it to Presenter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class MainPresenter { private MainView mMainView = new MainView.Empty(); private int currentPokemonId = 1; //... //set mMainView //left arrow: currentPokemonId++ //right arrow: currentPokemonId-- public void pokeballDropped(final String droppedText) { if (Pokeball.isPokeballDropped(droppedText)) { mMainView.sendPokemonImageUrl(PokemonUrlCreator.createImageUrl(currentPokemonId), currentPokemonId); } } } |
1 2 3 4 5 6 7 8 | public class Pokeball { private static final String POKEBALL = "POKEBALL"; public static boolean isPokeballDropped(final String droppedText) { return POKEBALL.equals(droppedText); } } |
1 2 3 4 5 6 | public class PokemonUrlCreator { public static String createImageUrl(final int pokemonId) { return "http://pokeapi.co/media/sprites/pokemon/" + pokemonId + ".png"; } } |
As you can see if dropped text is “POKEBALL” it means that some app tries to catch Pokémon. The appropriate link with the image is created and sent to Android system.
1 2 3 4 5 6 7 8 9 10 11 | public class MainActivity extends AppCompatActivity implements MainView { @Override public void sendPokemonImageUrl(final String imageLinkUrl, final int pokemonId) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(imageLinkUrl)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.putExtra(PokemonConstants.POKEMON_ID, pokemonId); intent.putExtra(PokemonConstants.POKEBALL_IMAGE_URL, imageLinkUrl); startActivity(intent); } } |
We could end it here – Android system will search which app will be able to open that link and it should find at least Chrome. We can also register one of our Activity from the previous app with pokéball to save caught Pokémon.
Save caught Pokémon
We have to go back to MultiWindowCatch app, register URL and save Pokémon.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <activity android:name="com.example.makor.multiwindowcatch.ui.main.MainActivity" android:configChanges="screenSize|screenLayout|smallestScreenSize|orientation"> //... <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="http" android:host="pokeapi.co" android:pathPrefix="/media" /> </intent-filter> </activity> |
Now after the drop, in-app chooser, our app also will be visible. The last step is to save Pokémon image URL and id.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public class MainActivity extends AppCompatActivity implements MainView, Adapter.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { //... checkIfFromDeeplinkAndSave(); } private void checkIfFromDeeplinkAndSave() { final Bundle extras = getIntent().getExtras(); if (extras != null) { mPresenter.checkIfFromDeeplinkAndSave( extras.getString(PokemonConstants.POKEBALL_IMAGE_URL), extras.getInt(PokemonConstants.POKEMON_ID)); } } @Override protected void onStart() { super.onStart(); mPresenter.setView(this); } @Override protected void onStop() { super.onStop(); mPresenter.clearView(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class MainPresenter { public void checkIfFromDeeplinkAndSave(final String pokemonImageUrl, final int pokemonId) { if (pokemonImageUrl != null && pokemonId != 0) { final Realm realm = Realm.getDefaultInstance(); realm.beginTransaction(); final Pokemon pokemon = realm.createObject(Pokemon.class); pokemon.setImageUrl(pokemonImageUrl); pokemon.setId(pokemonId); realm.commitTransaction(); } } } |
Checking if extras are from deep link simply comes down to checking if both image URL and id were sent. I used Realm to save them – Realm is very fast and the object is plain, so I do it in the main thread. List of Pokémon is displayed in onStart() with the new one on the beginning of the list.
Summary
Implementing drag and drop functionality during Multi-Window mode is very similar to what we had previously. In one app we have to start dragging and in another get the dropped data. This example is very simple and it clarifies few important points like permissions (what if I don’t want to get dropped data from another app?) or a way of sending data between two apps.
My complete two projects can be found in MultiWindowCatch and MultiWindowDropAdjacent repositories.
Ready to take your business to the next level with a digital product?
We'll be with you every step of the way, from idea to launch and beyond!