Skip to content

Commit bde69cd

Browse files
authored
Merge pull request #1296 from Neamar/real-drag-drop
Real drag and drop, where you can see the target moving.
2 parents 68df906 + eca0754 commit bde69cd

File tree

2 files changed

+132
-126
lines changed

2 files changed

+132
-126
lines changed

app/src/main/java/fr/neamar/kiss/DataHandler.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -719,8 +719,7 @@ public void setFavoritePosition(MainActivity context, String id, int position) {
719719
position = Math.min(position, favAppsList.size() - 1);
720720

721721
favAppsList.remove(currentPos);
722-
// Because we're removing ourselves from the array, positions may change, we should take that into account
723-
favAppsList.add(currentPos > position ? position + 1 : position, id);
722+
favAppsList.add(position, id);
724723
String newFavList = TextUtils.join(";", favAppsList);
725724

726725
PreferenceManager.getDefaultSharedPreferences(context).edit()

app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java

Lines changed: 131 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import android.view.View;
1717
import android.view.ViewGroup;
1818
import android.widget.ImageView;
19+
import android.widget.LinearLayout;
1920

2021
import androidx.annotation.NonNull;
2122

@@ -63,13 +64,12 @@ private static class ViewHolder {
6364
* Globals for drag and drop support
6465
*/
6566
private static long startTime = 0; // Start of the drag and drop, used for long press menu
66-
private float currentX = 0.0f; // Current X position of the drag op, this is 0 on DRAG END so we keep a copy here
67-
private ViewHolder overApp; // the view for the DRAG_END event is typically wrong, so we store a reference of the last dragged over app.
6867

6968
// Use so we don't over process on the drag events.
7069
private boolean mDragEnabled = true;
7170
private boolean isDragging = false;
7271
private boolean contextMenuShown = false;
72+
private int potentialNewIndex = -1;
7373
private int favCount = -1;
7474

7575
private SharedPreferences notificationPrefs = null;
@@ -137,18 +137,18 @@ void onFavoriteChange() {
137137
holders.add(viewHolder);
138138

139139
// We don't have enough data in our current ViewHolder, add a new one
140-
if(i >= currentFavCount) {
140+
if (i >= currentFavCount) {
141141
favoritesBar.addView(viewHolder.view);
142-
}
143-
else {
142+
} else {
144143
// Check if view is different
145144
View currentView = favoritesBar.getChildAt(i);
146-
if(currentView != viewHolder.view) {
147-
if(viewHolder.view.getParent() != null) {
145+
if (currentView != viewHolder.view) {
146+
if (viewHolder.view.getParent() != null) {
147+
// We need to remove the view from its parent first
148148
((ViewGroup) viewHolder.view.getParent()).removeView(viewHolder.view);
149149
}
150150
favoritesBar.addView(viewHolder.view, i);
151-
};
151+
}
152152
}
153153

154154
if (notificationPrefs != null) {
@@ -169,7 +169,10 @@ void onFavoriteChange() {
169169
}
170170

171171
// Remove any leftover views from previous renders
172-
for(int i = favCount; i < favoritesBar.getChildCount(); i++) {
172+
for (int i = favCount; i < favoritesBar.getChildCount(); i++) {
173+
View toBeDisposed = favoritesBar.getChildAt(i);
174+
toBeDisposed.setOnDragListener(null);
175+
toBeDisposed.setOnTouchListener(null);
173176
favoritesBar.removeViewAt(i);
174177
}
175178

@@ -194,73 +197,6 @@ void onDataSetChanged() {
194197
}
195198
}
196199

197-
/**
198-
* On first run, fill the favorite bar with sensible defaults
199-
*/
200-
private void addDefaultAppsToFavs() {
201-
{
202-
// Default phone call app
203-
Intent phoneIntent = new Intent(Intent.ACTION_DIAL);
204-
phoneIntent.setData(Uri.parse("tel:0000"));
205-
ResolveInfo resolveInfo = mainActivity.getPackageManager().resolveActivity(phoneIntent, PackageManager.MATCH_DEFAULT_ONLY);
206-
if (resolveInfo != null) {
207-
String packageName = resolveInfo.activityInfo.packageName;
208-
Log.i(TAG, "Dialer resolves to:" + packageName + "/" + resolveInfo.activityInfo.name);
209-
210-
if (resolveInfo.activityInfo.name != null && !resolveInfo.activityInfo.name.equals(DEFAULT_RESOLVER)) {
211-
String activityName = resolveInfo.activityInfo.name;
212-
if (packageName.equals("com.google.android.dialer")) {
213-
// Default dialer has two different activities, one when calling a phone number and one when opening the app from the launcher.
214-
// (com.google.android.apps.dialer.extensions.GoogleDialtactsActivity vs. com.google.android.dialer.extensions.GoogleDialtactsActivity)
215-
// (notice the .apps. in the middle)
216-
// The phoneIntent above resolve to the former, which isn't exposed as a Launcher activity and thus can't be displayed as a favorite
217-
// This hack uses the correct resolver when the application id is the default dialer.
218-
// In terms of maintenance, if Android was to change the name of the phone's main resolver, the favorite would simply not appear
219-
// and we would have to update the String below to the new default resolver
220-
activityName = "com.google.android.dialer.extensions.GoogleDialtactsActivity";
221-
}
222-
KissApplication.getApplication(mainActivity).getDataHandler().addToFavorites("app://" + packageName + "/" + activityName);
223-
}
224-
}
225-
}
226-
{
227-
// Default contacts app
228-
Intent contactsIntent = new Intent(Intent.ACTION_DEFAULT, ContactsContract.Contacts.CONTENT_URI);
229-
ResolveInfo resolveInfo = mainActivity.getPackageManager().resolveActivity(contactsIntent, PackageManager.MATCH_DEFAULT_ONLY);
230-
if (resolveInfo != null) {
231-
String packageName = resolveInfo.activityInfo.packageName;
232-
Log.i(TAG, "Contacts resolves to:" + packageName);
233-
if (resolveInfo.activityInfo.name != null && !resolveInfo.activityInfo.name.equals(DEFAULT_RESOLVER)) {
234-
KissApplication.getApplication(mainActivity).getDataHandler().addToFavorites("app://" + packageName + "/" + resolveInfo.activityInfo.name);
235-
}
236-
}
237-
238-
}
239-
{
240-
// Default browser
241-
Intent browserIntent = new Intent("android.intent.action.VIEW", Uri.parse("http://"));
242-
ResolveInfo resolveInfo = mainActivity.getPackageManager().resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY);
243-
if (resolveInfo != null) {
244-
String packageName = resolveInfo.activityInfo.packageName;
245-
Log.i(TAG, "Browser resolves to:" + packageName);
246-
247-
if (resolveInfo.activityInfo.name != null && !resolveInfo.activityInfo.name.equals(DEFAULT_RESOLVER)) {
248-
String activityName = resolveInfo.activityInfo.name;
249-
if (packageName.equalsIgnoreCase("com.android.chrome")) {
250-
// Chrome has two different activities, one for Launcher and one when opening an URL.
251-
// The browserIntent above resolve to the latter, which isn't exposed as a Launcher activity and thus can't be displayed
252-
// This hack uses the correct resolver when the application is Chrome.
253-
// In terms of maintenance, if Chrome was to change the name of the main resolver, the favorite would simply not appear
254-
// and we would have to update the String below to the new default resolver
255-
activityName = "com.google.android.apps.chrome.Main";
256-
}
257-
KissApplication.getApplication(mainActivity).getDataHandler().addToFavorites("app://" + packageName + "/" + activityName);
258-
}
259-
}
260-
}
261-
mainActivity.onFavoriteChange();
262-
}
263-
264200
@Override
265201
public void onClick(View v) {
266202
ViewHolder viewHolder = (ViewHolder) v.getTag();
@@ -317,7 +253,7 @@ public boolean onTouch(View view, MotionEvent motionEvent) {
317253
int MOVE_SENSITIVITY = 8;
318254
boolean hasMoved = (Math.abs(intCurrentX - intStartX) > MOVE_SENSITIVITY) || (Math.abs(intCurrentY - intStartY) > MOVE_SENSITIVITY);
319255

320-
if (hasMoved && mDragEnabled) {
256+
if (hasMoved && mDragEnabled && !isDragging) {
321257
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
322258

323259
if (contextMenuShown) {
@@ -327,10 +263,13 @@ public boolean onTouch(View view, MotionEvent motionEvent) {
327263
mDragEnabled = false;
328264
mainActivity.dismissPopup();
329265
mainActivity.closeContextMenu();
266+
267+
mainActivity.favoritesBar.setOnDragListener(this);
330268
View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);
331-
view.startDrag(null, shadowBuilder, view, 0);
332269
view.setVisibility(View.INVISIBLE);
333270
isDragging = true;
271+
view.startDrag(null, shadowBuilder, view, 0);
272+
Log.e("WTF", "Starting drag of " + ((ViewHolder) view.getTag()).pojo.id);
334273
return true;
335274
} else if (!contextMenuShown && !isDragging) {
336275
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
@@ -345,71 +284,139 @@ public boolean onTouch(View view, MotionEvent motionEvent) {
345284
}
346285

347286
@Override
348-
public boolean onDrag(View v, final DragEvent event) {
287+
public boolean onDrag(View targetView, final DragEvent event) {
288+
final View draggedView = (View) event.getLocalState();
349289

290+
String[] actions = new String[]{"", "started", "location", "drop", "ended", "entered", "exited"};
291+
Log.e("WTF", "Drag " + actions[event.getAction()] + " / " + targetView.toString());
350292
switch (event.getAction()) {
351293
case DragEvent.ACTION_DRAG_STARTED:
352-
// Inform the system that we are interested in being a potential drop target
353-
return true;
294+
return targetView instanceof LinearLayout;
354295
case DragEvent.ACTION_DRAG_ENTERED:
355-
case DragEvent.ACTION_DRAG_EXITED:
356-
case DragEvent.ACTION_DROP:
357-
if (!isDragging) {
358-
return true;
296+
return isDragging;
297+
case DragEvent.ACTION_DRAG_LOCATION:
298+
ViewGroup bar = ((ViewGroup) targetView);
299+
float x = event.getX();
300+
int width = targetView.getWidth();
301+
302+
int currentPos = (int) (favCount * x / width);
303+
304+
View currentChildAtPos = bar.getChildAt(currentPos);
305+
if(currentChildAtPos != draggedView) {
306+
bar.removeView(draggedView);
307+
try {
308+
bar.addView(draggedView, currentPos);
309+
}
310+
catch(IllegalStateException e) {
311+
// In some situations,
312+
// removeView() somehow fails (this especially happens if you start the drag and immediately moves to the left or right)
313+
// and we can't add the children back, because it still has a parent. In this case, do nothing, this should fix itself on the next iteration.
314+
potentialNewIndex = -1;
315+
return false;
316+
}
359317
}
360-
overApp = (ViewHolder) v.getTag();
361-
currentX = (event.getX() != 0.0f) ? event.getX() : currentX;
362318

363-
break;
319+
potentialNewIndex = currentPos;
364320

321+
return true;
322+
case DragEvent.ACTION_DROP:
323+
// Accept the drop, will be followed by ACTION_DRAG_ENDED
324+
return isDragging;
365325
case DragEvent.ACTION_DRAG_ENDED:
366-
// Only need to handle this action once.
367-
if (!isDragging) {
368-
return true;
326+
// Sometimes we don't trigger onDrag over another app, in which case just drop.
327+
if (potentialNewIndex == -1) {
328+
Log.w(TAG, "Wasn't dragged over a favorite, returning app to starting position");
329+
} else {
330+
final ViewHolder draggedApp = (ViewHolder) draggedView.getTag();
331+
int newIndex = potentialNewIndex;
332+
draggedView.post(() -> {
333+
// Signals to a View that the drag and drop operation has concluded.
334+
// If event result is set, this means the dragged view was dropped in target
335+
if (event.getResult()) {
336+
KissApplication.getApplication(mainActivity).getDataHandler().setFavoritePosition(mainActivity, draggedApp.result.getPojoId(), newIndex);
337+
mainActivity.onFavoriteChange();
338+
}
339+
});
369340
}
370-
isDragging = false;
371341

372342
// Reset dragging to what it should be
343+
draggedView.setVisibility(View.VISIBLE);
373344
mDragEnabled = favCount > 1;
345+
potentialNewIndex = -1;
346+
isDragging = false;
347+
return true;
348+
default:
349+
break;
350+
}
351+
return isDragging;
352+
}
374353

375-
final View draggedView = (View) event.getLocalState();
376-
377-
// Sometimes we don't trigger onDrag over another app, in which case just drop.
378-
if (overApp == null) {
379-
Log.w(TAG, "Wasn't dragged over an app, returning app to starting position");
380-
draggedView.post(() -> draggedView.setVisibility(View.VISIBLE));
381-
break;
382-
}
383-
384-
final ViewHolder draggedApp = (ViewHolder) draggedView.getTag();
385-
386-
int left = v.getLeft();
387-
int right = v.getRight();
388-
int width = right - left;
389354

390-
// currentX is relative to the view not the screen, so add the current X of the view.
391-
final boolean leftSide = (left + currentX < left + (width / 2));
355+
/**
356+
* On first run, fill the favorite bar with sensible defaults
357+
*/
358+
private void addDefaultAppsToFavs() {
359+
{
360+
// Default phone call app
361+
Intent phoneIntent = new Intent(Intent.ACTION_DIAL);
362+
phoneIntent.setData(Uri.parse("tel:0000"));
363+
ResolveInfo resolveInfo = mainActivity.getPackageManager().resolveActivity(phoneIntent, PackageManager.MATCH_DEFAULT_ONLY);
364+
if (resolveInfo != null) {
365+
String packageName = resolveInfo.activityInfo.packageName;
366+
Log.i(TAG, "Dialer resolves to:" + packageName + "/" + resolveInfo.activityInfo.name);
392367

393-
final int pos = KissApplication.getApplication(mainActivity).getDataHandler().getFavoritePosition(overApp.result.getPojoId());
368+
if (resolveInfo.activityInfo.name != null && !resolveInfo.activityInfo.name.equals(DEFAULT_RESOLVER)) {
369+
String activityName = resolveInfo.activityInfo.name;
370+
if (packageName.equals("com.google.android.dialer")) {
371+
// Default dialer has two different activities, one when calling a phone number and one when opening the app from the launcher.
372+
// (com.google.android.apps.dialer.extensions.GoogleDialtactsActivity vs. com.google.android.dialer.extensions.GoogleDialtactsActivity)
373+
// (notice the .apps. in the middle)
374+
// The phoneIntent above resolve to the former, which isn't exposed as a Launcher activity and thus can't be displayed as a favorite
375+
// This hack uses the correct resolver when the application id is the default dialer.
376+
// In terms of maintenance, if Android was to change the name of the phone's main resolver, the favorite would simply not appear
377+
// and we would have to update the String below to the new default resolver
378+
activityName = "com.google.android.dialer.extensions.GoogleDialtactsActivity";
379+
}
380+
KissApplication.getApplication(mainActivity).getDataHandler().addToFavorites("app://" + packageName + "/" + activityName);
381+
}
382+
}
383+
}
384+
{
385+
// Default contacts app
386+
Intent contactsIntent = new Intent(Intent.ACTION_DEFAULT, ContactsContract.Contacts.CONTENT_URI);
387+
ResolveInfo resolveInfo = mainActivity.getPackageManager().resolveActivity(contactsIntent, PackageManager.MATCH_DEFAULT_ONLY);
388+
if (resolveInfo != null) {
389+
String packageName = resolveInfo.activityInfo.packageName;
390+
Log.i(TAG, "Contacts resolves to:" + packageName);
391+
if (resolveInfo.activityInfo.name != null && !resolveInfo.activityInfo.name.equals(DEFAULT_RESOLVER)) {
392+
KissApplication.getApplication(mainActivity).getDataHandler().addToFavorites("app://" + packageName + "/" + resolveInfo.activityInfo.name);
393+
}
394+
}
394395

395-
draggedView.post(() -> {
396-
// Signals to a View that the drag and drop operation has concluded.
397-
// If event result is set, this means the dragged view was dropped in target
398-
if (event.getResult()) {
399-
KissApplication.getApplication(mainActivity).getDataHandler().setFavoritePosition(mainActivity, draggedApp.result.getPojoId(), leftSide ? pos - 1 : pos);
400-
draggedView.post(() -> draggedView.setVisibility(View.VISIBLE));
396+
}
397+
{
398+
// Default browser
399+
Intent browserIntent = new Intent("android.intent.action.VIEW", Uri.parse("http://"));
400+
ResolveInfo resolveInfo = mainActivity.getPackageManager().resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY);
401+
if (resolveInfo != null) {
402+
String packageName = resolveInfo.activityInfo.packageName;
403+
Log.i(TAG, "Browser resolves to:" + packageName);
401404

402-
mainActivity.onFavoriteChange();
403-
} else {
404-
draggedView.setVisibility(View.VISIBLE);
405+
if (resolveInfo.activityInfo.name != null && !resolveInfo.activityInfo.name.equals(DEFAULT_RESOLVER)) {
406+
String activityName = resolveInfo.activityInfo.name;
407+
if (packageName.equalsIgnoreCase("com.android.chrome")) {
408+
// Chrome has two different activities, one for Launcher and one when opening an URL.
409+
// The browserIntent above resolve to the latter, which isn't exposed as a Launcher activity and thus can't be displayed
410+
// This hack uses the correct resolver when the application is Chrome.
411+
// In terms of maintenance, if Chrome was to change the name of the main resolver, the favorite would simply not appear
412+
// and we would have to update the String below to the new default resolver
413+
activityName = "com.google.android.apps.chrome.Main";
405414
}
406-
});
407-
408-
break;
409-
default:
410-
break;
415+
KissApplication.getApplication(mainActivity).getDataHandler().addToFavorites("app://" + packageName + "/" + activityName);
416+
}
417+
}
411418
}
412-
return true;
419+
mainActivity.onFavoriteChange();
413420
}
414421
}
415422

0 commit comments

Comments
 (0)