From 00f1ea75b0a01eee3d5ece23e59610d6b4586c60 Mon Sep 17 00:00:00 2001 From: Florian Edelmann Date: Sun, 23 Feb 2014 17:58:18 +0100 Subject: [CMFM] bookmarks and history in a navigation drawer * used a modified version of DrawerLayout from the Support Library * moved bookmarks and history there -> always accessible * added a drawer icon * made all themeable * removed the BookmarksActivity and HistoryActivity * removed the BookmarksAdapter and HistoryAdapter as they are useless for LinearLayouts. I couldn't use ListViews because they scroll vertically - as well as the drawer itself. * removed the layouts for bookmarks and history Patch Set 2: tab cleanup and reset code I didn't touch Patch Set 3: fixed a FC after changing the theme when settings were opened by the drawer's overflow menu Patch Set 4: updated commit message Patch Set 5: updated commit message Patch Set 6: disabled "clear history" ActionBar action when history is empty Patch Set 7: fixed code style, fixed theme change issue with history items in the drawer, updated theme previews Patch Set 8: remove trailing whitespaces Change-Id: I215211e771ec7c96a2a669cdcc440556c310d6db --- AndroidManifest.xml | 4 +- .../ic_holo_light_navigation_drawer.png | Bin 0 -> 97 bytes .../ic_holo_light_navigation_drawer.png | Bin 0 -> 88 bytes res/drawable-nodpi/theme_preview.png | Bin 66556 -> 62470 bytes .../ic_holo_light_navigation_drawer.png | Bin 0 -> 107 bytes .../ic_holo_light_navigation_drawer.png | Bin 0 -> 120 bytes res/layout/bookmarks.xml | 36 - res/layout/bookmarks_item.xml | 94 +- res/layout/history.xml | 56 - res/layout/history_item.xml | 99 +- res/layout/navigation.xml | 68 +- res/layout/navigation_drawer.xml | 62 + res/layout/navigation_view_statusbar.xml | 20 - res/menu/drawer.xml | 41 + res/menu/navigation.xml | 8 - res/raw/changelog | 4 + res/values/strings.xml | 4 + res/values/styles.xml | 22 +- res/values/theme.xml | 6 + .../filemanager/activities/BookmarksActivity.java | 631 -------- .../filemanager/activities/HistoryActivity.java | 412 ----- .../filemanager/activities/NavigationActivity.java | 817 ++++++++-- .../filemanager/activities/SearchActivity.java | 2 +- .../preferences/SettingsPreferences.java | 1 + .../filemanager/adapters/BookmarksAdapter.java | 230 --- .../filemanager/adapters/HistoryAdapter.java | 218 --- .../cyanogenmod/filemanager/ui/ThemeManager.java | 19 + .../filemanager/ui/dialogs/ActionsDialog.java | 14 +- .../ui/policy/BookmarksActionPolicy.java | 17 +- .../ui/widgets/ActionBarDrawerToggle.java | 554 +++++++ .../filemanager/ui/widgets/DrawerLayout.java | 1621 ++++++++++++++++++++ .../filemanager/ui/widgets/ViewDragHelper.java | 1450 +++++++++++++++++ .../ic_holo_dark_navigation_drawer.png | Bin 0 -> 100 bytes .../ic_holo_dark_navigation_drawer.png | Bin 0 -> 91 bytes themes/res/drawable-nodpi/dark_theme_preview.png | Bin 64772 -> 59876 bytes .../ic_holo_dark_navigation_drawer.png | Bin 0 -> 109 bytes .../ic_holo_dark_navigation_drawer.png | Bin 0 -> 123 bytes themes/res/values/dark_theme.xml | 6 + 38 files changed, 4633 insertions(+), 1883 deletions(-) create mode 100644 res/drawable-hdpi/ic_holo_light_navigation_drawer.png create mode 100644 res/drawable-mdpi/ic_holo_light_navigation_drawer.png create mode 100644 res/drawable-xhdpi/ic_holo_light_navigation_drawer.png create mode 100644 res/drawable-xxhdpi/ic_holo_light_navigation_drawer.png delete mode 100644 res/layout/bookmarks.xml delete mode 100644 res/layout/history.xml create mode 100644 res/layout/navigation_drawer.xml create mode 100644 res/menu/drawer.xml delete mode 100644 src/com/cyanogenmod/filemanager/activities/BookmarksActivity.java delete mode 100644 src/com/cyanogenmod/filemanager/activities/HistoryActivity.java delete mode 100644 src/com/cyanogenmod/filemanager/adapters/BookmarksAdapter.java delete mode 100644 src/com/cyanogenmod/filemanager/adapters/HistoryAdapter.java create mode 100644 src/com/cyanogenmod/filemanager/ui/widgets/ActionBarDrawerToggle.java create mode 100644 src/com/cyanogenmod/filemanager/ui/widgets/DrawerLayout.java create mode 100644 src/com/cyanogenmod/filemanager/ui/widgets/ViewDragHelper.java create mode 100644 themes/res/drawable-hdpi/ic_holo_dark_navigation_drawer.png create mode 100644 themes/res/drawable-mdpi/ic_holo_dark_navigation_drawer.png create mode 100644 themes/res/drawable-xhdpi/ic_holo_dark_navigation_drawer.png create mode 100644 themes/res/drawable-xxhdpi/ic_holo_dark_navigation_drawer.png diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4c1ec934..bdb70ab9 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="102" + android:versionName="1.0.2"> diff --git a/res/drawable-hdpi/ic_holo_light_navigation_drawer.png b/res/drawable-hdpi/ic_holo_light_navigation_drawer.png new file mode 100644 index 00000000..3f1933b0 Binary files /dev/null and b/res/drawable-hdpi/ic_holo_light_navigation_drawer.png differ diff --git a/res/drawable-mdpi/ic_holo_light_navigation_drawer.png b/res/drawable-mdpi/ic_holo_light_navigation_drawer.png new file mode 100644 index 00000000..93e1d5e3 Binary files /dev/null and b/res/drawable-mdpi/ic_holo_light_navigation_drawer.png differ diff --git a/res/drawable-nodpi/theme_preview.png b/res/drawable-nodpi/theme_preview.png index f385813a..ba98ce0b 100644 Binary files a/res/drawable-nodpi/theme_preview.png and b/res/drawable-nodpi/theme_preview.png differ diff --git a/res/drawable-xhdpi/ic_holo_light_navigation_drawer.png b/res/drawable-xhdpi/ic_holo_light_navigation_drawer.png new file mode 100644 index 00000000..b9529d04 Binary files /dev/null and b/res/drawable-xhdpi/ic_holo_light_navigation_drawer.png differ diff --git a/res/drawable-xxhdpi/ic_holo_light_navigation_drawer.png b/res/drawable-xxhdpi/ic_holo_light_navigation_drawer.png new file mode 100644 index 00000000..a4bf10dd Binary files /dev/null and b/res/drawable-xxhdpi/ic_holo_light_navigation_drawer.png differ diff --git a/res/layout/bookmarks.xml b/res/layout/bookmarks.xml deleted file mode 100644 index 5ae41a9c..00000000 --- a/res/layout/bookmarks.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/bookmarks_item.xml b/res/layout/bookmarks_item.xml index fc8e4be5..2070494a 100644 --- a/res/layout/bookmarks_item.xml +++ b/res/layout/bookmarks_item.xml @@ -1,5 +1,6 @@ - +--> - + - + - + - + - + + - - + - \ No newline at end of file + \ No newline at end of file diff --git a/res/layout/history.xml b/res/layout/history.xml deleted file mode 100644 index 45a2993b..00000000 --- a/res/layout/history.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/history_item.xml b/res/layout/history_item.xml index 2ef5fe97..ad987140 100644 --- a/res/layout/history_item.xml +++ b/res/layout/history_item.xml @@ -1,5 +1,6 @@ - +--> - + - + - + - + - + + - + android:id="@+id/history_item_position" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginLeft="@dimen/default_margin" + android:paddingRight="@dimen/extra_margin" + android:singleLine="true" + android:textAppearance="@style/primary_text_appearance" + android:textStyle="normal" /> - \ No newline at end of file + \ No newline at end of file diff --git a/res/layout/navigation.xml b/res/layout/navigation.xml index 68df9c4f..051f35d6 100644 --- a/res/layout/navigation.xml +++ b/res/layout/navigation.xml @@ -1,5 +1,6 @@ - - - - - - +--> - - - - - - - + android:layout_height="match_parent" > + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/navigation_drawer.xml b/res/layout/navigation_drawer.xml new file mode 100644 index 00000000..9398ede5 --- /dev/null +++ b/res/layout/navigation_drawer.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/navigation_view_statusbar.xml b/res/layout/navigation_view_statusbar.xml index 3474265c..47ba2ac9 100644 --- a/res/layout/navigation_view_statusbar.xml +++ b/res/layout/navigation_view_statusbar.xml @@ -60,26 +60,6 @@ android:src="@drawable/ic_holo_light_search" android:visibility="invisible" /> - - - - diff --git a/res/menu/drawer.xml b/res/menu/drawer.xml new file mode 100644 index 00000000..7a5ef1d5 --- /dev/null +++ b/res/menu/drawer.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/res/menu/navigation.xml b/res/menu/navigation.xml index 9aef8541..5111c94f 100644 --- a/res/menu/navigation.xml +++ b/res/menu/navigation.xml @@ -17,14 +17,6 @@ - - CyanogenMod + + Open navigation drawer + Close navigation drawer + Alpha diff --git a/res/values/styles.xml b/res/values/styles.xml index b6cd4a32..81b80a6a 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -15,16 +15,14 @@ limitations under the License. --> - + + - + - + - + + + + + \ No newline at end of file diff --git a/res/values/theme.xml b/res/values/theme.xml index f6f4006d..d5128c91 100644 --- a/res/values/theme.xml +++ b/res/values/theme.xml @@ -83,6 +83,12 @@ @color/search_highlight + + #ffffffff + + + @drawable/ic_holo_light_navigation_drawer + @drawable/ic_holo_light_breadcrumb_divider diff --git a/src/com/cyanogenmod/filemanager/activities/BookmarksActivity.java b/src/com/cyanogenmod/filemanager/activities/BookmarksActivity.java deleted file mode 100644 index 68a8fedf..00000000 --- a/src/com/cyanogenmod/filemanager/activities/BookmarksActivity.java +++ /dev/null @@ -1,631 +0,0 @@ -/* - * Copyright (C) 2012 The CyanogenMod Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.cyanogenmod.filemanager.activities; - -import android.app.ActionBar; -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; -import android.content.res.XmlResourceParser; -import android.database.Cursor; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.storage.StorageVolume; -import android.util.Log; -import android.view.KeyEvent; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import com.android.internal.util.XmlUtils; -import com.cyanogenmod.filemanager.FileManagerApplication; -import com.cyanogenmod.filemanager.R; -import com.cyanogenmod.filemanager.adapters.BookmarksAdapter; -import com.cyanogenmod.filemanager.console.NoSuchFileOrDirectory; -import com.cyanogenmod.filemanager.model.Bookmark; -import com.cyanogenmod.filemanager.model.Bookmark.BOOKMARK_TYPE; -import com.cyanogenmod.filemanager.model.FileSystemObject; -import com.cyanogenmod.filemanager.preferences.AccessMode; -import com.cyanogenmod.filemanager.preferences.Bookmarks; -import com.cyanogenmod.filemanager.preferences.FileManagerSettings; -import com.cyanogenmod.filemanager.preferences.Preferences; -import com.cyanogenmod.filemanager.ui.ThemeManager; -import com.cyanogenmod.filemanager.ui.ThemeManager.Theme; -import com.cyanogenmod.filemanager.ui.dialogs.InitialDirectoryDialog; -import com.cyanogenmod.filemanager.ui.widgets.FlingerListView; -import com.cyanogenmod.filemanager.ui.widgets.FlingerListView.OnItemFlingerListener; -import com.cyanogenmod.filemanager.ui.widgets.FlingerListView.OnItemFlingerResponder; -import com.cyanogenmod.filemanager.util.CommandHelper; -import com.cyanogenmod.filemanager.util.DialogHelper; -import com.cyanogenmod.filemanager.util.ExceptionUtil; -import com.cyanogenmod.filemanager.util.StorageHelper; - -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -/** - * An activity for show bookmarks and links. - */ -public class BookmarksActivity extends Activity implements OnItemClickListener, OnClickListener { - - private static final String TAG = "BookmarksActivity"; //$NON-NLS-1$ - - private static boolean DEBUG = false; - - /** - * A listener for flinging events from {@link FlingerListView} - */ - private final OnItemFlingerListener mOnItemFlingerListener = new OnItemFlingerListener() { - - @Override - public boolean onItemFlingerStart( - AdapterView parent, View view, int position, long id) { - try { - // Response if the item can be removed - BookmarksAdapter adapter = (BookmarksAdapter)parent.getAdapter(); - Bookmark bookmark = adapter.getItem(position); - if (bookmark != null && - bookmark.mType.compareTo(BOOKMARK_TYPE.USER_DEFINED) == 0) { - return true; - } - } catch (Exception e) { - ExceptionUtil.translateException(BookmarksActivity.this, e, true, false); - } - return false; - } - - @Override - public void onItemFlingerEnd(OnItemFlingerResponder responder, - AdapterView parent, View view, int position, long id) { - - try { - // Response if the item can be removed - BookmarksAdapter adapter = (BookmarksAdapter)parent.getAdapter(); - Bookmark bookmark = adapter.getItem(position); - if (bookmark != null && - bookmark.mType.compareTo(BOOKMARK_TYPE.USER_DEFINED) == 0) { - boolean result = Bookmarks.removeBookmark(BookmarksActivity.this, bookmark); - if (!result) { - //Show warning - DialogHelper.showToast(BookmarksActivity.this, - R.string.msgs_operation_failure, Toast.LENGTH_SHORT); - responder.cancel(); - return; - } - responder.accept(); - adapter.remove(bookmark); - return; - } - - // Cancels the flinger operation - responder.cancel(); - - } catch (Exception e) { - ExceptionUtil.translateException(BookmarksActivity.this, e, true, false); - responder.cancel(); - } - } - }; - - private final BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent != null) { - if (intent.getAction().compareTo(FileManagerSettings.INTENT_THEME_CHANGED) == 0) { - applyTheme(); - } - } - } - }; - - // Bookmark list XML tags - private static final String TAG_BOOKMARKS = "Bookmarks"; //$NON-NLS-1$ - private static final String TAG_BOOKMARK = "bookmark"; //$NON-NLS-1$ - - /** - * @hide - */ - ListView mBookmarksListView; - - private boolean mChRooted; - - /** - * {@inheritDoc} - */ - @Override - protected void onCreate(Bundle state) { - if (DEBUG) { - Log.d(TAG, "BookmarksActivity.onCreate"); //$NON-NLS-1$ - } - - // Register the broadcast receiver - IntentFilter filter = new IntentFilter(); - filter.addAction(FileManagerSettings.INTENT_THEME_CHANGED); - registerReceiver(this.mNotificationReceiver, filter); - - // Is ChRooted? - this.mChRooted = FileManagerApplication.getAccessMode().compareTo(AccessMode.SAFE) == 0; - - //Set in transition - overridePendingTransition(R.anim.translate_to_right_in, R.anim.hold_out); - - //Set the main layout of the activity - setContentView(R.layout.bookmarks); - - //Initialize action bars and data - initTitleActionBar(); - initBookmarks(); - - // Apply the theme - applyTheme(); - - //Save state - super.onCreate(state); - } - - /** - * {@inheritDoc} - */ - @Override - protected void onDestroy() { - if (DEBUG) { - Log.d(TAG, "BookmarksActivity.onDestroy"); //$NON-NLS-1$ - } - - // Unregister the receiver - try { - unregisterReceiver(this.mNotificationReceiver); - } catch (Throwable ex) { - /**NON BLOCK**/ - } - - //All destroy. Continue - super.onDestroy(); - } - - /** - * {@inheritDoc} - */ - @Override - protected void onPause() { - //Set out transition - overridePendingTransition(R.anim.hold_in, R.anim.translate_to_left_out); - super.onPause(); - } - - /** - * {@inheritDoc} - */ - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - } - - /** - * Method that initializes the titlebar of the activity. - */ - private void initTitleActionBar() { - //Configure the action bar options - getActionBar().setBackgroundDrawable( - getResources().getDrawable(R.drawable.bg_holo_titlebar)); - getActionBar().setDisplayOptions( - ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME); - getActionBar().setDisplayHomeAsUpEnabled(true); - View customTitle = getLayoutInflater().inflate(R.layout.simple_customtitle, null, false); - TextView title = (TextView)customTitle.findViewById(R.id.customtitle_title); - title.setText(R.string.bookmarks); - title.setContentDescription(getString(R.string.bookmarks)); - getActionBar().setCustomView(customTitle); - } - - /** - * Method that initializes the titlebar of the activity. - */ - private void initBookmarks() { - this.mBookmarksListView = (ListView)findViewById(R.id.bookmarks_listview); - List bookmarks = new ArrayList(); - BookmarksAdapter adapter = new BookmarksAdapter(this, bookmarks, this); - this.mBookmarksListView.setAdapter(adapter); - this.mBookmarksListView.setOnItemClickListener(this); - - // If we should set the listview to response to flinger gesture detection - boolean useFlinger = - Preferences.getSharedPreferences().getBoolean( - FileManagerSettings.SETTINGS_USE_FLINGER.getId(), - ((Boolean)FileManagerSettings. - SETTINGS_USE_FLINGER. - getDefaultValue()).booleanValue()); - if (useFlinger) { - ((FlingerListView)this.mBookmarksListView). - setOnItemFlingerListener(this.mOnItemFlingerListener); - } - - // Reload the data - refresh(); - } - - /** - * Method that makes the refresh of the data. - */ - void refresh() { - // Retrieve the loading view - final View waiting = findViewById(R.id.bookmarks_waiting); - final BookmarksAdapter adapter = (BookmarksAdapter)this.mBookmarksListView.getAdapter(); - - // Load the history in background - AsyncTask task = new AsyncTask() { - Exception mCause; - List mBookmarks; - - @Override - protected Boolean doInBackground(Void... params) { - try { - this.mBookmarks = loadBookmarks(); - return Boolean.TRUE; - - } catch (Exception e) { - this.mCause = e; - return Boolean.FALSE; - } - } - - @Override - protected void onPreExecute() { - waiting.setVisibility(View.VISIBLE); - adapter.clear(); - } - - @Override - protected void onPostExecute(Boolean result) { - waiting.setVisibility(View.GONE); - if (result.booleanValue()) { - adapter.addAll(this.mBookmarks); - BookmarksActivity.this.mBookmarksListView.setSelection(0); - - } else { - if (this.mCause != null) { - ExceptionUtil.translateException(BookmarksActivity.this, this.mCause); - } - } - } - - @Override - protected void onCancelled() { - waiting.setVisibility(View.GONE); - } - }; - task.execute(); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_BACK: - back(true, null); - return true; - default: - return super.onKeyUp(keyCode, event); - } - } - - /** - * {@inheritDoc} - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - back(true, null); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Bookmark bookmark = ((BookmarksAdapter)parent.getAdapter()).getItem(position); - back(false, bookmark.mPath); - } - - /** - * {@inheritDoc} - */ - @Override - public void onClick(View v) { - //Retrieve the position - final int position = ((Integer)v.getTag()).intValue(); - final BookmarksAdapter adapter = (BookmarksAdapter)this.mBookmarksListView.getAdapter(); - final Bookmark bookmark = adapter.getItem(position); - - //Configure home - if (bookmark.mType.compareTo(BOOKMARK_TYPE.HOME) == 0) { - //Show a dialog for configure initial directory - InitialDirectoryDialog dialog = new InitialDirectoryDialog(this); - dialog.setOnValueChangedListener(new InitialDirectoryDialog.OnValueChangedListener() { - @Override - public void onValueChanged(String newInitialDir) { - adapter.getItem(position).mPath = newInitialDir; - adapter.notifyDataSetChanged(); - } - }); - dialog.show(); - return; - } - - //Remove bookmark - if (bookmark.mType.compareTo(BOOKMARK_TYPE.USER_DEFINED) == 0) { - boolean result = Bookmarks.removeBookmark(this, bookmark); - if (!result) { - //Show warning - DialogHelper.showToast(this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT); - return; - } - adapter.remove(bookmark); - return; - } - } - - /** - * Method that returns to previous activity and. - * - * @param cancelled Indicates if the activity was cancelled - * @param path The path of the selected bookmark - */ - private void back(final boolean cancelled, final String path) { - Intent intent = new Intent(); - if (cancelled) { - setResult(RESULT_CANCELED, intent); - } else { - // Check that the bookmark exists - try { - FileSystemObject fso = CommandHelper.getFileInfo(this, path, null); - if (fso != null) { - intent.putExtra(NavigationActivity.EXTRA_BOOKMARK_SELECTION, fso); - setResult(RESULT_OK, intent); - } else { - // The bookmark not exists, delete the user-defined bookmark - try { - Bookmark b = Bookmarks.getBookmark(getContentResolver(), path); - Bookmarks.removeBookmark(this, b); - refresh(); - } catch (Exception ex) {/**NON BLOCK**/} - } - } catch (Exception e) { - // Capture the exception - ExceptionUtil.translateException(this, e); - if (e instanceof NoSuchFileOrDirectory || e instanceof FileNotFoundException) { - // The bookmark not exists, delete the user-defined bookmark - try { - Bookmark b = Bookmarks.getBookmark(getContentResolver(), path); - Bookmarks.removeBookmark(this, b); - refresh(); - } catch (Exception ex) {/**NON BLOCK**/} - } - return; - } - } - finish(); - } - - /** - * Method that loads all kind of bookmarks and join in - * an array to be used in the listview adapter. - * - * @return List - * @hide - */ - List loadBookmarks() { - // Bookmarks = HOME + FILESYSTEM + SD STORAGES + USER DEFINED - // In ChRooted mode = SD STORAGES + USER DEFINED (from SD STORAGES) - List bookmarks = new ArrayList(); - if (!this.mChRooted) { - bookmarks.add(loadHomeBookmarks()); - bookmarks.addAll(loadFilesystemBookmarks()); - } - bookmarks.addAll(loadSdStorageBookmarks()); - bookmarks.addAll(loadUserBookmarks()); - return bookmarks; - } - - /** - * Method that loads the home bookmark from the user preference. - * - * @return Bookmark The bookmark loaded - */ - private Bookmark loadHomeBookmarks() { - String initialDir = Preferences.getSharedPreferences().getString( - FileManagerSettings.SETTINGS_INITIAL_DIR.getId(), - (String)FileManagerSettings.SETTINGS_INITIAL_DIR.getDefaultValue()); - return new Bookmark(BOOKMARK_TYPE.HOME, getString(R.string.bookmarks_home), initialDir); - } - - /** - * Method that loads the filesystem bookmarks from the internal xml file. - * (defined by this application) - * - * @return List The bookmarks loaded - */ - private List loadFilesystemBookmarks() { - try { - //Initialize the bookmarks - List bookmarks = new ArrayList(); - - //Read the command list xml file - XmlResourceParser parser = getResources().getXml(R.xml.filesystem_bookmarks); - - try { - //Find the root element - XmlUtils.beginDocument(parser, TAG_BOOKMARKS); - while (true) { - XmlUtils.nextElement(parser); - String element = parser.getName(); - if (element == null) { - break; - } - - if (TAG_BOOKMARK.equals(element)) { - CharSequence name = null; - CharSequence directory = null; - - try { - name = - getString(parser.getAttributeResourceValue( - R.styleable.Bookmark_name, 0)); - } catch (Exception e) {/**NON BLOCK**/} - try { - directory = - getString(parser.getAttributeResourceValue( - R.styleable.Bookmark_directory, 0)); - } catch (Exception e) {/**NON BLOCK**/} - if (directory == null) { - directory = - parser.getAttributeValue(R.styleable.Bookmark_directory); - } - if (name != null && directory != null) { - bookmarks.add( - new Bookmark( - BOOKMARK_TYPE.FILESYSTEM, - name.toString(), - directory.toString())); - } - } - } - - //Return the bookmarks - return bookmarks; - - } finally { - parser.close(); - } - } catch (Throwable ex) { - Log.e(TAG, "Load filesystem bookmarks failed", ex); //$NON-NLS-1$ - } - - //No data - return new ArrayList(); - } - - /** - * Method that loads the secure digital card storage bookmarks from the system. - * - * @return List The bookmarks loaded - */ - private List loadSdStorageBookmarks() { - //Initialize the bookmarks - List bookmarks = new ArrayList(); - - try { - //Recovery sdcards from storage manager - StorageVolume[] volumes = StorageHelper.getStorageVolumes(getApplication()); - int cc = volumes.length; - for (int i = 0; i < cc ; i++) { - if (volumes[i].getPath().toLowerCase(Locale.ROOT).indexOf("usb") != -1) { //$NON-NLS-1$ - bookmarks.add( - new Bookmark( - BOOKMARK_TYPE.USB, - StorageHelper.getStorageVolumeDescription( - getApplication(), volumes[i]), - volumes[i].getPath())); - } else { - bookmarks.add( - new Bookmark( - BOOKMARK_TYPE.SDCARD, - StorageHelper.getStorageVolumeDescription( - getApplication(), volumes[i]), - volumes[i].getPath())); - } - } - - //Return the bookmarks - return bookmarks; - } catch (Throwable ex) { - Log.e(TAG, "Load filesystem bookmarks failed", ex); //$NON-NLS-1$ - } - - //No data - return new ArrayList(); - } - - /** - * Method that loads the user bookmarks (added by the user). - * - * @return List The bookmarks loaded - */ - private List loadUserBookmarks() { - List bookmarks = new ArrayList(); - Cursor cursor = Bookmarks.getAllBookmarks(this.getContentResolver()); - try { - if (cursor != null && cursor.moveToFirst()) { - do { - Bookmark bm = new Bookmark(cursor); - if (this.mChRooted && !StorageHelper.isPathInStorageVolume(bm.mPath)) { - continue; - } - bookmarks.add(bm); - } while (cursor.moveToNext()); - } - } finally { - try { - if (cursor != null) { - cursor.close(); - } - } catch (Exception e) {/**NON BLOCK**/} - } - return bookmarks; - } - - /** - * Method that applies the current theme to the activity - * @hide - */ - void applyTheme() { - Theme theme = ThemeManager.getCurrentTheme(this); - theme.setBaseTheme(this, false); - - //- ActionBar - theme.setTitlebarDrawable(this, getActionBar(), "titlebar_drawable"); //$NON-NLS-1$ - View v = getActionBar().getCustomView().findViewById(R.id.customtitle_title); - theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$ - // -View - theme.setBackgroundDrawable( - this, this.mBookmarksListView, "background_drawable"); //$NON-NLS-1$ - if (((BookmarksAdapter)this.mBookmarksListView.getAdapter()) != null) { - ((BookmarksAdapter)this.mBookmarksListView.getAdapter()).notifyThemeChanged(); - ((BookmarksAdapter)this.mBookmarksListView.getAdapter()).notifyDataSetChanged(); - } - this.mBookmarksListView.setDivider( - theme.getDrawable(this, "horizontal_divider_drawable")); //$NON-NLS-1$ - this.mBookmarksListView.invalidate(); - } -} diff --git a/src/com/cyanogenmod/filemanager/activities/HistoryActivity.java b/src/com/cyanogenmod/filemanager/activities/HistoryActivity.java deleted file mode 100644 index 84de995c..00000000 --- a/src/com/cyanogenmod/filemanager/activities/HistoryActivity.java +++ /dev/null @@ -1,412 +0,0 @@ -/* - * Copyright (C) 2012 The CyanogenMod Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.cyanogenmod.filemanager.activities; - -import android.app.ActionBar; -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; -import android.os.AsyncTask; -import android.os.Bundle; -import android.util.Log; -import android.view.KeyEvent; -import android.view.MenuItem; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ImageView; -import android.widget.ListPopupWindow; -import android.widget.ListView; -import android.widget.TextView; - -import com.cyanogenmod.filemanager.R; -import com.cyanogenmod.filemanager.adapters.HighlightedSimpleMenuListAdapter; -import com.cyanogenmod.filemanager.adapters.HistoryAdapter; -import com.cyanogenmod.filemanager.adapters.SimpleMenuListAdapter; -import com.cyanogenmod.filemanager.model.History; -import com.cyanogenmod.filemanager.preferences.FileManagerSettings; -import com.cyanogenmod.filemanager.ui.ThemeManager; -import com.cyanogenmod.filemanager.ui.ThemeManager.Theme; -import com.cyanogenmod.filemanager.ui.widgets.ButtonItem; -import com.cyanogenmod.filemanager.util.AndroidHelper; -import com.cyanogenmod.filemanager.util.DialogHelper; -import com.cyanogenmod.filemanager.util.ExceptionUtil; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * An activity for show navigation history. - */ -public class HistoryActivity extends Activity implements OnItemClickListener { - - private static final String TAG = "HistoryActivity"; //$NON-NLS-1$ - - private static boolean DEBUG = false; - - private final BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent != null) { - if (intent.getAction().compareTo(FileManagerSettings.INTENT_THEME_CHANGED) == 0) { - applyTheme(); - } - } - } - }; - - /** - * @hide - */ - ListView mListView; - /** - * @hide - */ - HistoryAdapter mAdapter; - /** - * @hide - */ - boolean mIsEmpty; - private boolean mIsClearHistory; - - private View mOptionsAnchorView; - - /** - * Intent extra parameter for the history data. - */ - public static final String EXTRA_HISTORY_LIST = "extra_history_list"; //$NON-NLS-1$ - - /** - * {@inheritDoc} - */ - @Override - protected void onCreate(Bundle state) { - if (DEBUG) { - Log.d(TAG, "HistoryActivity.onCreate"); //$NON-NLS-1$ - } - - // Register the broadcast receiver - IntentFilter filter = new IntentFilter(); - filter.addAction(FileManagerSettings.INTENT_THEME_CHANGED); - registerReceiver(this.mNotificationReceiver, filter); - - this.mIsEmpty = false; - this.mIsClearHistory = false; - - //Set in transition - overridePendingTransition(R.anim.translate_to_right_in, R.anim.hold_out); - - //Set the main layout of the activity - setContentView(R.layout.history); - - //Initialize action bars and data - initTitleActionBar(); - initHistory(); - - // Apply the theme - applyTheme(); - - //Save state - super.onCreate(state); - } - - /** - * {@inheritDoc} - */ - @Override - protected void onDestroy() { - if (DEBUG) { - Log.d(TAG, "HistoryActivity.onDestroy"); //$NON-NLS-1$ - } - - // Unregister the receiver - try { - unregisterReceiver(this.mNotificationReceiver); - } catch (Throwable ex) { - /**NON BLOCK**/ - } - - //All destroy. Continue - super.onDestroy(); - } - - /** - * {@inheritDoc} - */ - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - } - - /** - * {@inheritDoc} - */ - @Override - protected void onPause() { - //Set out transition - overridePendingTransition(R.anim.hold_in, R.anim.translate_to_left_out); - super.onPause(); - } - - /** - * Method that initializes the titlebar of the activity. - */ - private void initTitleActionBar() { - //Configure the action bar options - getActionBar().setBackgroundDrawable( - getResources().getDrawable(R.drawable.bg_holo_titlebar)); - getActionBar().setDisplayOptions( - ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME); - getActionBar().setDisplayHomeAsUpEnabled(true); - View customTitle = getLayoutInflater().inflate(R.layout.simple_customtitle, null, false); - TextView title = (TextView)customTitle.findViewById(R.id.customtitle_title); - title.setText(R.string.history); - title.setContentDescription(getString(R.string.history)); - ButtonItem configuration = (ButtonItem)customTitle.findViewById(R.id.ab_button1); - configuration.setImageResource(R.drawable.ic_holo_light_overflow); - configuration.setContentDescription(getString(R.string.actionbar_button_overflow_cd)); - - View status = findViewById(R.id.history_status); - boolean showOptionsMenu = AndroidHelper.showOptionsMenu(getApplicationContext()); - configuration.setVisibility(showOptionsMenu ? View.VISIBLE : View.GONE); - this.mOptionsAnchorView = showOptionsMenu ? configuration : status; - - getActionBar().setCustomView(customTitle); - } - - /** - * Method invoked when an action item is clicked. - * - * @param view The button pushed - */ - public void onActionBarItemClick(View view) { - switch (view.getId()) { - case R.id.ab_button1: - //Overflow - showOverflowPopUp(view); - break; - - default: - break; - } - } - - /** - * Method that initializes the titlebar of the activity. - */ - @SuppressWarnings("unchecked") - private void initHistory() { - // Retrieve the loading view - final View waiting = findViewById(R.id.history_waiting); - - this.mListView = (ListView)findViewById(R.id.history_listview); - - // Load the history in background - AsyncTask> task = new AsyncTask>() { - Exception mCause; - List mHistory; - - @Override - protected List doInBackground(Void... params) { - try { - this.mHistory = - (List)getIntent().getSerializableExtra(EXTRA_HISTORY_LIST); - if (this.mHistory.isEmpty()) { - View msg = findViewById(R.id.history_empty_msg); - msg.setVisibility(View.VISIBLE); - return new ArrayList(); - } - HistoryActivity.this.mIsEmpty = this.mHistory.isEmpty(); - - //Show inverted history - final List adapterList = new ArrayList(this.mHistory); - Collections.reverse(adapterList); - return adapterList; - - } catch (Exception e) { - this.mCause = e; - return null; - } - } - - @Override - protected void onPreExecute() { - waiting.setVisibility(View.VISIBLE); - } - - @Override - protected void onPostExecute(List result) { - waiting.setVisibility(View.GONE); - if (result != null) { - HistoryActivity.this.mAdapter = - new HistoryAdapter(HistoryActivity.this, result); - - if (HistoryActivity.this.mListView != null && - HistoryActivity.this.mAdapter != null) { - - HistoryActivity.this.mListView. - setAdapter(HistoryActivity.this.mAdapter); - HistoryActivity.this.mListView. - setOnItemClickListener(HistoryActivity.this); - } - - } else { - if (this.mCause != null) { - ExceptionUtil.translateException(HistoryActivity.this, this.mCause); - } - } - } - - @Override - protected void onCancelled() { - waiting.setVisibility(View.GONE); - } - }; - task.execute(); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_MENU: - if (!this.mIsEmpty) { - showOverflowPopUp(this.mOptionsAnchorView); - } - return true; - case KeyEvent.KEYCODE_BACK: - back(true, null); - return true; - default: - return super.onKeyUp(keyCode, event); - } - } - - /** - * {@inheritDoc} - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - back(true, null); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - History history = ((HistoryAdapter)parent.getAdapter()).getItem(position); - back(false, history); - } - - /** - * Method that returns to previous activity and. - * - * @param cancelled Indicates if the activity was cancelled - * @param history The selected history - */ - private void back(final boolean cancelled, final History history) { - Intent intent = new Intent(); - if (cancelled) { - if (this.mIsClearHistory) { - intent.putExtra(NavigationActivity.EXTRA_HISTORY_CLEAR, true); - } - setResult(RESULT_CANCELED, intent); - } else { - intent.putExtra(NavigationActivity.EXTRA_HISTORY_ENTRY_SELECTION, history); - setResult(RESULT_OK, intent); - } - finish(); - } - - /** - * Method that clean the history and return back to navigation view - * @hide - */ - void clearHistory() { - if (this.mAdapter != null) { - this.mAdapter.clear(); - View msg = findViewById(R.id.history_empty_msg); - msg.setVisibility(View.VISIBLE); - this.mIsClearHistory = true; - } - } - - /** - * Method that shows a popup with the activity main menu. - * - * @param anchor The anchor of the popup - */ - private void showOverflowPopUp(View anchor) { - SimpleMenuListAdapter adapter = - new HighlightedSimpleMenuListAdapter(this, R.menu.history); - final ListPopupWindow popup = - DialogHelper.createListPopupWindow(this, adapter, anchor); - popup.setOnItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick( - final AdapterView parent, final View v, - final int position, final long id) { - final int itemId = (int)id; - switch (itemId) { - case R.id.mnu_clear_history: - popup.dismiss(); - clearHistory(); - break; - } - } - }); - popup.show(); - } - - /** - * Method that applies the current theme to the activity - * @hide - */ - void applyTheme() { - Theme theme = ThemeManager.getCurrentTheme(this); - theme.setBaseTheme(this, false); - - //- ActionBar - theme.setTitlebarDrawable(this, getActionBar(), "titlebar_drawable"); //$NON-NLS-1$ - View v = getActionBar().getCustomView().findViewById(R.id.customtitle_title); - theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$ - v = findViewById(R.id.ab_button1); - theme.setImageDrawable(this, (ImageView)v, "ab_overflow_drawable"); //$NON-NLS-1$ - // -View - theme.setBackgroundDrawable(this, this.mListView, "background_drawable"); //$NON-NLS-1$ - if (this.mAdapter != null) { - this.mAdapter.notifyThemeChanged(); - this.mAdapter.notifyDataSetChanged(); - } - this.mListView.setDivider( - theme.getDrawable(this, "horizontal_divider_drawable")); //$NON-NLS-1$ - this.mListView.invalidate(); - } -} diff --git a/src/com/cyanogenmod/filemanager/activities/NavigationActivity.java b/src/com/cyanogenmod/filemanager/activities/NavigationActivity.java index 3d97a04a..68852945 100644 --- a/src/com/cyanogenmod/filemanager/activities/NavigationActivity.java +++ b/src/com/cyanogenmod/filemanager/activities/NavigationActivity.java @@ -26,28 +26,47 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.nfc.NfcAdapter; import android.nfc.NfcEvent; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.os.storage.StorageVolume; +import android.transition.Visibility; import android.util.Log; +import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.ListAdapter; import android.widget.ListPopupWindow; +import android.widget.ListView; import android.widget.PopupWindow; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; +import com.android.internal.util.XmlUtils; import com.cyanogenmod.filemanager.FileManagerApplication; import com.cyanogenmod.filemanager.R; import com.cyanogenmod.filemanager.activities.preferences.SettingsPreferences; @@ -61,31 +80,40 @@ import com.cyanogenmod.filemanager.console.InsufficientPermissionsException; import com.cyanogenmod.filemanager.console.NoSuchFileOrDirectory; import com.cyanogenmod.filemanager.listeners.OnHistoryListener; import com.cyanogenmod.filemanager.listeners.OnRequestRefreshListener; +import com.cyanogenmod.filemanager.model.Bookmark; import com.cyanogenmod.filemanager.model.DiskUsage; import com.cyanogenmod.filemanager.model.FileSystemObject; import com.cyanogenmod.filemanager.model.History; import com.cyanogenmod.filemanager.model.MountPoint; +import com.cyanogenmod.filemanager.model.Bookmark.BOOKMARK_TYPE; import com.cyanogenmod.filemanager.parcelables.HistoryNavigable; import com.cyanogenmod.filemanager.parcelables.NavigationViewInfoParcelable; import com.cyanogenmod.filemanager.parcelables.SearchInfoParcelable; import com.cyanogenmod.filemanager.preferences.AccessMode; +import com.cyanogenmod.filemanager.preferences.Bookmarks; import com.cyanogenmod.filemanager.preferences.FileManagerSettings; import com.cyanogenmod.filemanager.preferences.NavigationLayoutMode; import com.cyanogenmod.filemanager.preferences.ObjectIdentifier; import com.cyanogenmod.filemanager.preferences.Preferences; +import com.cyanogenmod.filemanager.ui.IconHolder; import com.cyanogenmod.filemanager.ui.ThemeManager; import com.cyanogenmod.filemanager.ui.ThemeManager.Theme; import com.cyanogenmod.filemanager.ui.dialogs.ActionsDialog; import com.cyanogenmod.filemanager.ui.dialogs.FilesystemInfoDialog; +import com.cyanogenmod.filemanager.ui.dialogs.InitialDirectoryDialog; import com.cyanogenmod.filemanager.ui.dialogs.FilesystemInfoDialog.OnMountListener; +import com.cyanogenmod.filemanager.ui.widgets.ActionBarDrawerToggle; import com.cyanogenmod.filemanager.ui.widgets.Breadcrumb; import com.cyanogenmod.filemanager.ui.widgets.ButtonItem; +import com.cyanogenmod.filemanager.ui.widgets.DrawerLayout; +import com.cyanogenmod.filemanager.ui.widgets.FlingerListView; import com.cyanogenmod.filemanager.ui.widgets.NavigationCustomTitleView; import com.cyanogenmod.filemanager.ui.widgets.NavigationView; import com.cyanogenmod.filemanager.ui.widgets.NavigationView.OnNavigationRequestMenuListener; import com.cyanogenmod.filemanager.ui.widgets.NavigationView.OnNavigationSelectionChangedListener; import com.cyanogenmod.filemanager.ui.widgets.SelectionView; import com.cyanogenmod.filemanager.util.AndroidHelper; +import com.cyanogenmod.filemanager.util.BookmarksHelper; import com.cyanogenmod.filemanager.util.CommandHelper; import com.cyanogenmod.filemanager.util.DialogHelper; import com.cyanogenmod.filemanager.util.ExceptionUtil; @@ -99,6 +127,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; /** * The main navigation activity. This activity is the center of the application. @@ -120,39 +149,19 @@ public class NavigationActivity extends Activity private static boolean DEBUG = false; - /** - * Intent code for request a bookmark selection. - */ - public static final int INTENT_REQUEST_BOOKMARK = 10001; - - /** - * Intent code for request a history selection. - */ - public static final int INTENT_REQUEST_HISTORY = 20001; + // Bookmark list XML tags + private static final String TAG_BOOKMARKS = "Bookmarks"; //$NON-NLS-1$ + private static final String TAG_BOOKMARK = "bookmark"; //$NON-NLS-1$ /** * Intent code for request a search. */ - public static final int INTENT_REQUEST_SEARCH = 30001; - - - /** - * Constant for extra information about selected bookmark. - */ - public static final String EXTRA_BOOKMARK_SELECTION = - "extra_bookmark_selection"; //$NON-NLS-1$ + public static final int INTENT_REQUEST_SEARCH = 10001; /** - * Constant for extra information about selected history entry. - */ - public static final String EXTRA_HISTORY_ENTRY_SELECTION = - "extra_history_entry_selection"; //$NON-NLS-1$ - - /** - * Constant for extra information about clear selection action. + * Intent code for request a search. */ - public static final String EXTRA_HISTORY_CLEAR = - "extra_history_clear_history"; //$NON-NLS-1$ + public static final int INTENT_REQUEST_SETTINGS = 20001; /** * Constant for extra information about selected search entry. @@ -297,6 +306,15 @@ public class NavigationActivity extends Activity private ViewGroup mActionBar; private SelectionView mSelectionBar; + private DrawerLayout mDrawerLayout; + private ScrollView mDrawer; + private ActionBarDrawerToggle mDrawerToggle; + private LinearLayout mDrawerHistory; + private TextView mDrawerHistoryEmpty; + + private List mBookmarks; + private LinearLayout mDrawerBookmarks; + private boolean mExitFlag = false; private long mExitBackTimeout = -1; @@ -373,6 +391,10 @@ public class NavigationActivity extends Activity initStatusActionBar(); initSelectionBar(); + // Initialize navigation drawer + initDrawer(); + initBookmarks(); + // Adjust layout (only when start on landscape mode) int orientation = getResources().getConfiguration().orientation; if (orientation == Configuration.ORIENTATION_LANDSCAPE) { @@ -408,6 +430,13 @@ public class NavigationActivity extends Activity super.onCreate(state); } + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + // Sync the toggle state after onRestoreInstanceState has occurred. + mDrawerToggle.syncState(); + } + /** * {@inheritDoc} */ @@ -427,6 +456,7 @@ public class NavigationActivity extends Activity public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); onLayoutChanged(); + mDrawerToggle.onConfigurationChanged(newConfig); } /** @@ -488,9 +518,12 @@ public class NavigationActivity extends Activity //Display the welcome message? if (firstUse) { - AlertDialog dialog = DialogHelper.createAlertDialog( - this, R.drawable.ic_launcher, - R.string.welcome_title, getString(R.string.welcome_msg), false); + // open navigation drawer to show user that it exists + mDrawerLayout.openDrawer(mDrawer); + + AlertDialog dialog = DialogHelper.createAlertDialog(this, + R.drawable.ic_launcher, R.string.welcome_title, + getString(R.string.welcome_msg), false); DialogHelper.delegateDialogShow(this, dialog); // Don't display again this dialog @@ -505,6 +538,8 @@ public class NavigationActivity extends Activity * Method that initializes the titlebar of the activity. */ private void initTitleActionBar() { + getActionBar().setTitle(R.string.app_name); + //Inflate the view and associate breadcrumb View titleLayout = getLayoutInflater().inflate( R.layout.navigation_view_customtitle, null, false); @@ -584,6 +619,504 @@ public class NavigationActivity extends Activity this.mSelectionBar = (SelectionView)findViewById(R.id.navigation_selectionbar); } + /** + * Method that initializes the navigation drawer of the activity. + */ + private void initDrawer() { + mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); + mDrawer = (ScrollView) findViewById(R.id.drawer); + mDrawerBookmarks = (LinearLayout) findViewById(R.id.bookmarks_list); + mDrawerHistory = (LinearLayout) findViewById(R.id.history_list); + mDrawerHistoryEmpty = (TextView) findViewById(R.id.history_empty); + + // Set the navigation drawer "hamburger" icon + mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, + R.drawable.ic_holo_light_navigation_drawer, + R.string.drawer_open, R.string.drawer_close) { + + /** Called when a drawer has settled in a completely closed state. */ + public void onDrawerClosed(View view) { + super.onDrawerClosed(view); + getActionBar().setDisplayOptions( + ActionBar.DISPLAY_SHOW_CUSTOM + | ActionBar.DISPLAY_SHOW_HOME); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setHomeButtonEnabled(true); + invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu() + } + + /** Called when a drawer has settled in a completely open state. */ + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + getActionBar().setDisplayOptions( + ActionBar.DISPLAY_SHOW_TITLE + | ActionBar.DISPLAY_SHOW_HOME); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setHomeButtonEnabled(true); + + // change ActionBar title text color + Theme theme = ThemeManager + .getCurrentTheme(NavigationActivity.this); + // get ActionBar title TextView id + int titleId = Resources.getSystem().getIdentifier( + "action_bar_title", "id", "android"); + TextView v = (TextView) findViewById(titleId); + theme.setTextColor(NavigationActivity.this, v, "text_color"); //$NON-NLS-1$ + + invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu() + } + }; + + // Set the drawer toggle as the DrawerListener + mDrawerLayout.setDrawerListener(mDrawerToggle); + + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setHomeButtonEnabled(true); + } + + /** + * Method adds a history entry to the history list in the drawer + */ + private void addHistoryToDrawer(int index, HistoryNavigable navigable) { + // hide empty message + mDrawerHistoryEmpty.setVisibility(View.GONE); + + Theme theme = ThemeManager.getCurrentTheme(this); + IconHolder iconholder = new IconHolder(this, false); + + // inflate single bookmark layout item and fill it + LinearLayout view = (LinearLayout) getLayoutInflater().inflate( + R.layout.history_item, null); + + ImageView iconView = (ImageView) view + .findViewById(R.id.history_item_icon); + TextView name = (TextView) view.findViewById(R.id.history_item_name); + TextView directory = (TextView) view + .findViewById(R.id.history_item_directory); + TextView position = (TextView) view + .findViewById(R.id.history_item_position); + + // if (history.getItem() instanceof NavigationViewInfoParcelable) + Drawable icon = iconholder.getDrawable("ic_fso_folder_drawable"); //$NON-NLS-1$ + if (navigable instanceof SearchInfoParcelable) { + icon = iconholder.getDrawable("ic_history_search_drawable"); //$NON-NLS-1$ + } + iconView.setImageDrawable(icon); + + String title = navigable.getTitle(); + if (title == null || title.trim().length() == 0) { + title = getString(R.string.root_directory_name); + } + + name.setText(title); + directory.setText(navigable.getDescription()); + position.setText(String.format("#%d", index + 1)); + + theme.setTextColor(this, name, "text_color"); + theme.setTextColor(this, directory, "text_color"); + theme.setTextColor(this, position, "text_color"); + + // handle item click + view.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + final int index = mDrawerHistory.indexOfChild(v); + final int count = mDrawerHistory.getChildCount(); + final History history = mHistory.get(count - index - 1); + + navigateToHistory(history); + mDrawerLayout.closeDrawer(mDrawer); + } + }); + + // add as first child + mDrawerHistory.addView(view, 0); + } + + /** + * Method takes a bookmark as argument and adds it to mBookmarks and the + * list in the drawer + */ + public void addBookmark(Bookmark bookmark) { + mBookmarks.add(bookmark); + addBookmarkToDrawer(bookmark); + } + + /** + * Method takes a bookmark as argument and adds it to the bookmark list in + * the drawer + */ + private void addBookmarkToDrawer(Bookmark bookmark) { + Theme theme = ThemeManager.getCurrentTheme(this); + IconHolder iconholder = new IconHolder(this, false); + + // inflate single bookmark layout item and fill it + LinearLayout view = (LinearLayout) getLayoutInflater().inflate( + R.layout.bookmarks_item, null); + + ImageView icon = (ImageView) view + .findViewById(R.id.bookmarks_item_icon); + TextView name = (TextView) view.findViewById(R.id.bookmarks_item_name); + TextView path = (TextView) view.findViewById(R.id.bookmarks_item_path); + ImageButton actionButton = (ImageButton) view + .findViewById(R.id.bookmarks_item_action); + + name.setText(bookmark.mName); + path.setText(bookmark.mPath); + + theme.setTextColor(this, name, "text_color"); + theme.setTextColor(this, path, "text_color"); + + icon.setImageDrawable(iconholder.getDrawable(BookmarksHelper + .getIcon(bookmark))); + + Drawable action = null; + String actionCd = null; + if (bookmark.mType.compareTo(BOOKMARK_TYPE.HOME) == 0) { + action = iconholder.getDrawable("ic_config_drawable"); //$NON-NLS-1$ + actionCd = getApplicationContext().getString( + R.string.bookmarks_button_config_cd); + } + else if (bookmark.mType.compareTo(BOOKMARK_TYPE.USER_DEFINED) == 0) { + action = iconholder.getDrawable("ic_close_drawable"); //$NON-NLS-1$ + actionCd = getApplicationContext().getString( + R.string.bookmarks_button_remove_bookmark_cd); + } + + actionButton.setImageDrawable(action); + actionButton.setVisibility(action != null ? View.VISIBLE : View.GONE); + actionButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + final View v = (View) view.getParent(); + final int index = mDrawerBookmarks.indexOfChild(v); + final Bookmark bookmark = mBookmarks.get(index); + + // Configure home + if (bookmark.mType.compareTo(BOOKMARK_TYPE.HOME) == 0) { + // Show a dialog for configure initial directory + InitialDirectoryDialog dialog = new InitialDirectoryDialog( + NavigationActivity.this); + dialog.setOnValueChangedListener(new InitialDirectoryDialog.OnValueChangedListener() { + @Override + public void onValueChanged(String newInitialDir) { + bookmark.mPath = newInitialDir; + + // reset drawer bookmarks list + initBookmarks(); + } + }); + dialog.show(); + return; + } + + // Remove bookmark + if (bookmark.mType.compareTo(BOOKMARK_TYPE.USER_DEFINED) == 0) { + boolean result = Bookmarks.removeBookmark( + getApplicationContext(), bookmark); + if (!result) { // Show warning + DialogHelper.showToast(getApplicationContext(), + R.string.msgs_operation_failure, + Toast.LENGTH_SHORT); + return; + } + mBookmarks.remove(bookmark); + mDrawerBookmarks.removeView(v); + return; + } + } + }); + actionButton.setContentDescription(actionCd); + + // handle item click + view.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + final int index = mDrawerBookmarks.indexOfChild(v); + final Bookmark bookmark = mBookmarks.get(index); + + // try to navigate to the bookmark path + try { + FileSystemObject fso = CommandHelper.getFileInfo( + getApplicationContext(), bookmark.mPath, null); + if (fso != null) { + getCurrentNavigationView().open(fso); + mDrawerLayout.closeDrawer(mDrawer); + } + else { + // The bookmark does not exist, delete the user-defined + // bookmark + try { + Bookmarks.removeBookmark(getApplicationContext(), + bookmark); + + // reset bookmarks list to default + initBookmarks(); + } + catch (Exception ex) { + } + } + } + catch (Exception e) { // Capture the exception + ExceptionUtil + .translateException(NavigationActivity.this, e); + if (e instanceof NoSuchFileOrDirectory + || e instanceof FileNotFoundException) { + // The bookmark does not exist, delete the user-defined + // bookmark + try { + Bookmarks.removeBookmark(getApplicationContext(), + bookmark); + + // reset bookmarks list to default + initBookmarks(); + } + catch (Exception ex) { + } + } + return; + } + } + }); + + mDrawerBookmarks.addView(view); + } + + /** + * Method that initializes the bookmarks. + */ + private void initBookmarks() { + // Retrieve the loading view + final View waiting = findViewById(R.id.bookmarks_loading); + + // Load bookmarks in background + AsyncTask task = new AsyncTask() { + Exception mCause; + + @Override + protected Boolean doInBackground(Void... params) { + try { + mBookmarks = loadBookmarks(); + return Boolean.TRUE; + + } + catch (Exception e) { + this.mCause = e; + return Boolean.FALSE; + } + } + + @Override + protected void onPreExecute() { + waiting.setVisibility(View.VISIBLE); + mDrawerBookmarks.removeAllViews(); + } + + @Override + protected void onPostExecute(Boolean result) { + waiting.setVisibility(View.GONE); + if (result.booleanValue()) { + for (Bookmark bookmark : mBookmarks) { + addBookmarkToDrawer(bookmark); + } + } + else { + if (this.mCause != null) { + ExceptionUtil.translateException( + NavigationActivity.this, this.mCause); + } + } + } + + @Override + protected void onCancelled() { + waiting.setVisibility(View.GONE); + } + }; + task.execute(); + } + + /** + * Method that loads all kind of bookmarks and join in an array to be used + * in the listview adapter. + * + * @return List + * @hide + */ + List loadBookmarks() { + // Bookmarks = HOME + FILESYSTEM + SD STORAGES + USER DEFINED + // In ChRooted mode = SD STORAGES + USER DEFINED (from SD STORAGES) + List bookmarks = new ArrayList(); + if (!this.mChRooted) { + bookmarks.add(loadHomeBookmarks()); + bookmarks.addAll(loadFilesystemBookmarks()); + } + bookmarks.addAll(loadSdStorageBookmarks()); + bookmarks.addAll(loadUserBookmarks()); + return bookmarks; + } + + /** + * Method that loads the home bookmark from the user preference. + * + * @return Bookmark The bookmark loaded + */ + private Bookmark loadHomeBookmarks() { + String initialDir = Preferences.getSharedPreferences().getString( + FileManagerSettings.SETTINGS_INITIAL_DIR.getId(), + (String) FileManagerSettings.SETTINGS_INITIAL_DIR + .getDefaultValue()); + return new Bookmark(BOOKMARK_TYPE.HOME, + getString(R.string.bookmarks_home), initialDir); + } + + /** + * Method that loads the filesystem bookmarks from the internal xml file. + * (defined by this application) + * + * @return List The bookmarks loaded + */ + private List loadFilesystemBookmarks() { + try { + // Initialize the bookmarks + List bookmarks = new ArrayList(); + + // Read the command list xml file + XmlResourceParser parser = getResources().getXml( + R.xml.filesystem_bookmarks); + + try { + // Find the root element + XmlUtils.beginDocument(parser, TAG_BOOKMARKS); + while (true) { + XmlUtils.nextElement(parser); + String element = parser.getName(); + if (element == null) { + break; + } + + if (TAG_BOOKMARK.equals(element)) { + CharSequence name = null; + CharSequence directory = null; + + try { + name = getString(parser.getAttributeResourceValue( + R.styleable.Bookmark_name, 0)); + } + catch (Exception e) { + /** NON BLOCK **/ + } + try { + directory = getString(parser + .getAttributeResourceValue( + R.styleable.Bookmark_directory, 0)); + } + catch (Exception e) { + /** NON BLOCK **/ + } + if (directory == null) { + directory = parser + .getAttributeValue(R.styleable.Bookmark_directory); + } + if (name != null && directory != null) { + bookmarks.add(new Bookmark( + BOOKMARK_TYPE.FILESYSTEM, name.toString(), + directory.toString())); + } + } + } + + // Return the bookmarks + return bookmarks; + + } + finally { + parser.close(); + } + } + catch (Throwable ex) { + Log.e(TAG, "Load filesystem bookmarks failed", ex); //$NON-NLS-1$ + } + + // No data + return new ArrayList(); + } + + /** + * Method that loads the secure digital card storage bookmarks from the + * system. + * + * @return List The bookmarks loaded + */ + private List loadSdStorageBookmarks() { + // Initialize the bookmarks + List bookmarks = new ArrayList(); + + try { + // Recovery sdcards from storage manager + StorageVolume[] volumes = StorageHelper + .getStorageVolumes(getApplication()); + int cc = volumes.length; + for (int i = 0; i < cc; i++) { + if (volumes[i].getPath().toLowerCase(Locale.ROOT) + .indexOf("usb") != -1) { //$NON-NLS-1$ + bookmarks.add(new Bookmark(BOOKMARK_TYPE.USB, StorageHelper + .getStorageVolumeDescription(getApplication(), + volumes[i]), volumes[i].getPath())); + } + else { + bookmarks.add(new Bookmark(BOOKMARK_TYPE.SDCARD, + StorageHelper.getStorageVolumeDescription( + getApplication(), volumes[i]), volumes[i] + .getPath())); + } + } + + // Return the bookmarks + return bookmarks; + } + catch (Throwable ex) { + Log.e(TAG, "Load filesystem bookmarks failed", ex); //$NON-NLS-1$ + } + + // No data + return new ArrayList(); + } + + /** + * Method that loads the user bookmarks (added by the user). + * + * @return List The bookmarks loaded + */ + private List loadUserBookmarks() { + List bookmarks = new ArrayList(); + Cursor cursor = Bookmarks.getAllBookmarks(this.getContentResolver()); + try { + if (cursor != null && cursor.moveToFirst()) { + do { + Bookmark bm = new Bookmark(cursor); + if (this.mChRooted + && !StorageHelper.isPathInStorageVolume(bm.mPath)) { + continue; + } + bookmarks.add(bm); + } + while (cursor.moveToNext()); + } + } + finally { + try { + if (cursor != null) { + cursor.close(); + } + } + catch (Exception e) { + /** NON BLOCK **/ + } + } + return bookmarks; + } + /** * Method that initializes the navigation views of the activity */ @@ -820,16 +1353,58 @@ public class NavigationActivity extends Activity */ @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - if ((getActionBar().getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) - == ActionBar.DISPLAY_HOME_AS_UP) { - checkBackAction(); - } - return true; - default: - return super.onOptionsItemSelected(item); - } + // Pass the event to ActionBarDrawerToggle, if it returns + // true, then it has handled the app icon touch event + if (mDrawerToggle.onOptionsItemSelected(item)) { + return true; + } + + // just handle the drawer list here + switch (item.getItemId()) { + case R.id.mnu_actions_add_to_bookmarks_current_folder: + // TODO add bookmark + Log.d(TAG, "add bookmark"); + return true; + case R.id.mnu_clear_history: + clearHistory(); + return true; + case R.id.mnu_settings: + openSettings(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + /** + * Called when the menu is created. Just includes the drawer's overflow + * menu. All entries are hidden until onPrepareOptionsMenu unhides them. + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.drawer, menu); + return true; + } + + /** + * Called whenever we call invalidateOptionsMenu() + */ + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawer); + + for (int i = 0; i < menu.size(); i++) { + // show all items if drawer is open, + // hide them if not + menu.getItem(i).setVisible(drawerOpen); + + if (menu.getItem(i).getItemId() == R.id.mnu_clear_history) { + menu.getItem(i).setEnabled(mHistory.size() > 0); + } + } + + return super.onPrepareOptionsMenu(menu); } /** @@ -907,15 +1482,8 @@ public class NavigationActivity extends Activity //Action Bar buttons //###################### case R.id.ab_actions: - openActionsDialog(getCurrentNavigationView().getCurrentDir(), true); - break; - - case R.id.ab_bookmarks: - openBookmarks(); - break; - - case R.id.ab_history: - openHistory(); + openActionsDialog(getCurrentNavigationView().getCurrentDir(), + true); break; case R.id.ab_search: @@ -936,34 +1504,15 @@ public class NavigationActivity extends Activity */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == INTENT_REQUEST_SETTINGS) { + // reset bookmarks list to default as the user could changed the + // root mode which changes the system bookmarks + initBookmarks(); + return; + } + if (data != null) { switch (requestCode) { - case INTENT_REQUEST_BOOKMARK: - if (resultCode == RESULT_OK) { - FileSystemObject fso = - (FileSystemObject)data. - getSerializableExtra(EXTRA_BOOKMARK_SELECTION); - if (fso != null) { - //Open the fso - getCurrentNavigationView().open(fso); - } - } - break; - - case INTENT_REQUEST_HISTORY: - if (resultCode == RESULT_OK) { - //Change current directory - History history = - (History)data.getSerializableExtra(EXTRA_HISTORY_ENTRY_SELECTION); - navigateToHistory(history); - } else if (resultCode == RESULT_CANCELED) { - boolean clear = data.getBooleanExtra(EXTRA_HISTORY_CLEAR, false); - if (clear) { - clearHistory(); - } - } - break; - case INTENT_REQUEST_SEARCH: if (resultCode == RESULT_OK) { //Change directory? @@ -988,6 +1537,9 @@ public class NavigationActivity extends Activity getCurrentNavigationView().refresh(true); } } + // reset bookmarks list to default as the user could have set a + // new bookmark in the search activity + initBookmarks(); break; default: @@ -1001,11 +1553,10 @@ public class NavigationActivity extends Activity */ @Override public void onNewHistory(HistoryNavigable navigable) { + addHistoryToDrawer(this.mHistory.size(), navigable); //Recollect information about current status History history = new History(this.mHistory.size(), navigable); this.mHistory.add(history); - getActionBar().setDisplayHomeAsUpEnabled(true); - getActionBar().setHomeButtonEnabled(true); } /** @@ -1013,10 +1564,6 @@ public class NavigationActivity extends Activity */ @Override public void onCheckHistory() { - //Need to show HomeUp Button - boolean enabled = this.mHistory != null && this.mHistory.size() > 0; - getActionBar().setDisplayHomeAsUpEnabled(enabled); - getActionBar().setHomeButtonEnabled(enabled); } /** @@ -1184,21 +1731,7 @@ public class NavigationActivity extends Activity switch (itemId) { case R.id.mnu_settings: //Settings - Intent settings = new Intent( - NavigationActivity.this, SettingsPreferences.class); - startActivity(settings); - break; - - case R.id.mnu_history: - //History - openHistory(); - popup.dismiss(); - break; - - case R.id.mnu_bookmarks: - //Bookmarks - openBookmarks(); - popup.dismiss(); + openSettings(); break; case R.id.mnu_search: @@ -1307,7 +1840,8 @@ public class NavigationActivity extends Activity */ private void clearHistory() { this.mHistory.clear(); - onCheckHistory(); + mDrawerHistory.removeAllViews(); + mDrawerHistoryEmpty.setVisibility(View.VISIBLE); } /** @@ -1350,10 +1884,11 @@ public class NavigationActivity extends Activity int cc = realHistory.getPosition(); for (int i = this.mHistory.size() - 1; i >= cc; i--) { this.mHistory.remove(i); + mDrawerHistory.removeViewAt(0); } - if (this.mHistory.size() == 0) { - getActionBar().setDisplayHomeAsUpEnabled(false); - getActionBar().setHomeButtonEnabled(false); + + if (mDrawerHistory.getChildCount() == 0) { + mDrawerHistoryEmpty.setVisibility(View.VISIBLE); } //Navigate @@ -1460,45 +1995,37 @@ public class NavigationActivity extends Activity } // Show the dialog - ActionsDialog dialog = new ActionsDialog(this, fso, global, false); + ActionsDialog dialog = new ActionsDialog(this, this, fso, global, false); dialog.setOnRequestRefreshListener(this); dialog.setOnSelectionListener(getCurrentNavigationView()); dialog.show(); } /** - * Method that opens the bookmarks activity. - * @hide - */ - void openBookmarks() { - Intent bookmarksIntent = new Intent(this, BookmarksActivity.class); - bookmarksIntent.addFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME); - startActivityForResult(bookmarksIntent, INTENT_REQUEST_BOOKMARK); - } - - /** - * Method that opens the history activity. + * Method that opens the search activity. + * * @hide */ - void openHistory() { - Intent historyIntent = new Intent(this, HistoryActivity.class); - historyIntent.putExtra(HistoryActivity.EXTRA_HISTORY_LIST, (Serializable)this.mHistory); - historyIntent.addFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME); - startActivityForResult(historyIntent, INTENT_REQUEST_HISTORY); + void openSearch() { + onSearchRequested(); } /** - * Method that opens the search activity. + * Method that opens the settings activity. + * * @hide */ - void openSearch() { - onSearchRequested(); + void openSettings() { + Intent settingsIntent = new Intent(NavigationActivity.this, + SettingsPreferences.class); + startActivityForResult(settingsIntent, INTENT_REQUEST_SETTINGS); } /** * Method that remove the {@link FileSystemObject} from the history */ private void removeFromHistory(FileSystemObject fso) { + // TODO remove drawer entry here, too if (this.mHistory != null) { int cc = this.mHistory.size(); for (int i = cc-1; i >= 0 ; i--) { @@ -1631,12 +2158,18 @@ public class NavigationActivity extends Activity */ private void onLayoutChanged() { Theme theme = ThemeManager.getCurrentTheme(this); + boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawer); // Apply only when the orientation was changed int orientation = getResources().getConfiguration().orientation; if (this.mOrientation == orientation) return; this.mOrientation = orientation; + // imitate a closed drawer while layout is rebuilt to avoid NullPointerException + if (drawerOpen) { + mDrawerToggle.onDrawerClosed(mDrawer); + } + if (this.mOrientation == Configuration.ORIENTATION_LANDSCAPE) { // Landscape mode ViewGroup statusBar = (ViewGroup)findViewById(R.id.navigation_statusbar); @@ -1702,6 +2235,11 @@ public class NavigationActivity extends Activity // Show holder newParent.setVisibility(View.VISIBLE); } + + // if drawer was open, imitate reopening + if (drawerOpen) { + mDrawerToggle.onDrawerOpened(mDrawer); + } } /** @@ -1713,11 +2251,19 @@ public class NavigationActivity extends Activity Theme theme = ThemeManager.getCurrentTheme(this); theme.setBaseTheme(this, false); + // imitate a closed drawer while layout is rebuilt to avoid NullPointerException + boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawer); + if (drawerOpen) { + mDrawerToggle.onDrawerClosed(mDrawer); + } + //- Layout View v = findViewById(R.id.navigation_layout); theme.setBackgroundDrawable(this, v, "background_drawable"); //$NON-NLS-1$ + //- ActionBar theme.setTitlebarDrawable(this, getActionBar(), "titlebar_drawable"); //$NON-NLS-1$ + //- StatusBar v = findViewById(R.id.navigation_statusbar); if (orientation == Configuration.ORIENTATION_LANDSCAPE) { @@ -1731,10 +2277,7 @@ public class NavigationActivity extends Activity theme.setImageDrawable(this, (ImageView)v, "ab_actions_drawable"); //$NON-NLS-1$ v = findViewById(R.id.ab_search); theme.setImageDrawable(this, (ImageView)v, "ab_search_drawable"); //$NON-NLS-1$ - v = findViewById(R.id.ab_bookmarks); - theme.setImageDrawable(this, (ImageView)v, "ab_bookmarks_drawable"); //$NON-NLS-1$ - v = findViewById(R.id.ab_history); - theme.setImageDrawable(this, (ImageView)v, "ab_history_drawable"); //$NON-NLS-1$ + //- Expanders v = findViewById(R.id.ab_configuration); theme.setImageDrawable(this, (ImageView)v, "expander_open_drawable"); //$NON-NLS-1$ @@ -1746,6 +2289,7 @@ public class NavigationActivity extends Activity theme.setImageDrawable(this, (ImageView)v, "ab_layout_mode_drawable"); //$NON-NLS-1$ v = findViewById(R.id.ab_view_options); theme.setImageDrawable(this, (ImageView)v, "ab_view_options_drawable"); //$NON-NLS-1$ + //- SelectionBar v = findViewById(R.id.navigation_selectionbar); theme.setBackgroundDrawable(this, v, "selectionbar_drawable"); //$NON-NLS-1$ @@ -1753,11 +2297,38 @@ public class NavigationActivity extends Activity theme.setImageDrawable(this, (ImageView)v, "ab_selection_done_drawable"); //$NON-NLS-1$ v = findViewById(R.id.navigation_status_selection_label); theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$ + + // - Navigation drawer + theme.setBackgroundColor(this, mDrawer, "drawer_color"); + v = findViewById(R.id.bookmarks_header); + theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$ + v = findViewById(R.id.history_header); + theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$ + v = findViewById(R.id.history_empty); + theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$ + mDrawerToggle.setDrawerImageResource(theme.getResourceId(this, "drawer_icon")); + + for (int i=0; i { - - /** - * A class that conforms with the ViewHolder pattern to performance - * the list view rendering. - */ - private static class ViewHolder { - /** - * @hide - */ - public ViewHolder() { - super(); - } - ImageView mIvIcon; - TextView mTvName; - TextView mTvPath; - ImageButton mBtAction; - } - - /** - * A class that holds the full data information. - */ - private static class DataHolder { - /** - * @hide - */ - public DataHolder() { - super(); - } - Drawable mDwIcon; - String mName; - String mPath; - Drawable mDwAction; - String mActionCd; - } - - - - private DataHolder[] mData; - private IconHolder mIconHolder; - private final OnClickListener mOnActionClickListener; - - //The resource item layout - private static final int RESOURCE_LAYOUT = R.layout.bookmarks_item; - - //The resource of the item icon - private static final int RESOURCE_ITEM_ICON = R.id.bookmarks_item_icon; - //The resource of the item name - private static final int RESOURCE_ITEM_NAME = R.id.bookmarks_item_name; - //The resource of the item directory - private static final int RESOURCE_ITEM_PATH = R.id.bookmarks_item_path; - //The resource of the item button action - private static final int RESOURCE_ITEM_ACTION = R.id.bookmarks_item_action; - - /** - * Constructor of BookmarksAdapter. - * - * @param context The current context - * @param bookmarks The bookmarks - * @param onActionClickListener The listener for listen action clicks - */ - public BookmarksAdapter( - Context context, List bookmarks, OnClickListener onActionClickListener) { - super(context, RESOURCE_ITEM_NAME, bookmarks); - this.mIconHolder = new IconHolder(context, false); - this.mOnActionClickListener = onActionClickListener; - - //Do cache of the data for better performance - processData(bookmarks); - } - - /** - * {@inheritDoc} - */ - @Override - public void notifyDataSetChanged() { - processData(null); - super.notifyDataSetChanged(); - } - - /** - * Method that dispose the elements of the adapter. - */ - public void dispose() { - clear(); - this.mData = null; - if (mIconHolder != null) { - mIconHolder.cleanup(); - mIconHolder = null; - } - } - - /** - * Method that process the data before use {@link #getView} method. - * - * @param bookmarks The list of bookmarks (to better performance) or null. - */ - private void processData(List bookmarks) { - this.mData = new DataHolder[getCount()]; - int cc = (bookmarks == null) ? getCount() : bookmarks.size(); - for (int i = 0; i < cc; i++) { - //Bookmark info - Bookmark bookmark = (bookmarks == null) ? getItem(i) : bookmarks.get(i); - - //Build the data holder - this.mData[i] = new BookmarksAdapter.DataHolder(); - this.mData[i].mDwIcon = - this.mIconHolder.getDrawable(BookmarksHelper.getIcon(bookmark)); - this.mData[i].mName = bookmark.mName; - this.mData[i].mPath = bookmark.mPath; - this.mData[i].mDwAction = null; - this.mData[i].mActionCd = null; - if (bookmark.mType.compareTo(BOOKMARK_TYPE.HOME) == 0) { - this.mData[i].mDwAction = - this.mIconHolder.getDrawable("ic_config_drawable"); //$NON-NLS-1$ - this.mData[i].mActionCd = - getContext().getString(R.string.bookmarks_button_config_cd); - } else if (bookmark.mType.compareTo(BOOKMARK_TYPE.USER_DEFINED) == 0) { - this.mData[i].mDwAction = - this.mIconHolder.getDrawable("ic_close_drawable"); //$NON-NLS-1$ - this.mData[i].mActionCd = - getContext().getString(R.string.bookmarks_button_remove_bookmark_cd); - } - } - } - - /** - * {@inheritDoc} - */ - @Override - public View getView(int position, View convertView, ViewGroup parent) { - - //Check to reuse view - View v = convertView; - if (v == null) { - //Create the view holder - LayoutInflater li = - (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - v = li.inflate(RESOURCE_LAYOUT, parent, false); - ViewHolder viewHolder = new BookmarksAdapter.ViewHolder(); - viewHolder.mIvIcon = (ImageView)v.findViewById(RESOURCE_ITEM_ICON); - viewHolder.mTvName = (TextView)v.findViewById(RESOURCE_ITEM_NAME); - viewHolder.mTvPath = (TextView)v.findViewById(RESOURCE_ITEM_PATH); - viewHolder.mBtAction = (ImageButton)v.findViewById(RESOURCE_ITEM_ACTION); - viewHolder.mBtAction.setTag(Integer.valueOf(position)); - v.setTag(viewHolder); - - // Apply the current theme - Theme theme = ThemeManager.getCurrentTheme(getContext()); - theme.setBackgroundDrawable( - getContext(), v, "selectors_deselected_drawable"); //$NON-NLS-1$ - theme.setTextColor( - getContext(), viewHolder.mTvName, "text_color"); //$NON-NLS-1$ - theme.setTextColor( - getContext(), viewHolder.mTvPath, "text_color"); //$NON-NLS-1$ - } - - //Retrieve data holder - final DataHolder dataHolder = this.mData[position]; - - //Retrieve the view holder - ViewHolder viewHolder = (ViewHolder)v.getTag(); - - //Set the data - viewHolder.mIvIcon.setImageDrawable(dataHolder.mDwIcon); - viewHolder.mTvName.setText(dataHolder.mName); - viewHolder.mTvPath.setText(dataHolder.mPath); - boolean hasAction = dataHolder.mDwAction != null; - viewHolder.mBtAction.setImageDrawable(hasAction ? dataHolder.mDwAction : null); - viewHolder.mBtAction.setVisibility(hasAction ? View.VISIBLE : View.GONE); - viewHolder.mBtAction.setOnClickListener(this.mOnActionClickListener); - viewHolder.mBtAction.setContentDescription(dataHolder.mActionCd); - - //Return the view - return v; - } - - /** - * Method that should be invoked when the theme of the app was changed - */ - public void notifyThemeChanged() { - if (mIconHolder != null) { - mIconHolder.cleanup(); - } - // Empty icon holder - this.mIconHolder = new IconHolder(getContext(), false); - } -} diff --git a/src/com/cyanogenmod/filemanager/adapters/HistoryAdapter.java b/src/com/cyanogenmod/filemanager/adapters/HistoryAdapter.java deleted file mode 100644 index f8ebbf02..00000000 --- a/src/com/cyanogenmod/filemanager/adapters/HistoryAdapter.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (C) 2012 The CyanogenMod Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.cyanogenmod.filemanager.adapters; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import com.cyanogenmod.filemanager.R; -import com.cyanogenmod.filemanager.model.History; -import com.cyanogenmod.filemanager.parcelables.NavigationViewInfoParcelable; -import com.cyanogenmod.filemanager.parcelables.SearchInfoParcelable; -import com.cyanogenmod.filemanager.ui.IconHolder; -import com.cyanogenmod.filemanager.ui.ThemeManager; -import com.cyanogenmod.filemanager.ui.ThemeManager.Theme; - -import java.util.List; - -/** - * An implementation of {@link ArrayAdapter} for display history. - */ -public class HistoryAdapter extends ArrayAdapter { - - /** - * A class that conforms with the ViewHolder pattern to performance - * the list view rendering. - */ - private static class ViewHolder { - /** - * @hide - */ - public ViewHolder() { - super(); - } - ImageView mIvIcon; - TextView mTvName; - TextView mTvDirectory; - TextView mTvPosition; - } - - /** - * A class that holds the full data information. - */ - private static class DataHolder { - /** - * @hide - */ - public DataHolder() { - super(); - } - Drawable mDwIcon; - String mName; - String mDirectory; - String mPosition; - } - - - - private DataHolder[] mData; - private IconHolder mIconHolder; - - //The resource item layout - private static final int RESOURCE_LAYOUT = R.layout.history_item; - - //The resource of the item icon - private static final int RESOURCE_ITEM_ICON = R.id.history_item_icon; - //The resource of the item name - private static final int RESOURCE_ITEM_NAME = R.id.history_item_name; - //The resource of the item directory - private static final int RESOURCE_ITEM_DIRECTORY = R.id.history_item_directory; - //The resource of the item position - private static final int RESOURCE_ITEM_POSITION = R.id.history_item_position; - - /** - * Constructor of HistoryAdapter. - * - * @param context The current context - * @param history The history reference - */ - public HistoryAdapter(Context context, List history) { - super(context, RESOURCE_ITEM_NAME, history); - notifyThemeChanged(); // Reload icons - - //Do cache of the data for better performance - processData(history); - } - - /** - * {@inheritDoc} - */ - @Override - public void notifyDataSetChanged() { - processData(null); - super.notifyDataSetChanged(); - } - - /** - * Method that dispose the elements of the adapter. - */ - public void dispose() { - clear(); - this.mData = null; - if (mIconHolder != null) { - mIconHolder.cleanup(); - mIconHolder = null; - } - } - - /** - * Method that process the data before use {@link #getView} method. - * - * @param historyData The list of histories (to better performance) or null. - */ - private void processData(List historyData) { - this.mData = new DataHolder[getCount()]; - int cc = (historyData == null) ? getCount() : historyData.size(); - for (int i = 0; i < cc; i++) { - //History info - History history = (historyData == null) ? getItem(i) : historyData.get(i); - - //Build the data holder - this.mData[i] = new HistoryAdapter.DataHolder(); - if (history.getItem() instanceof NavigationViewInfoParcelable) { - this.mData[i].mDwIcon = - this.mIconHolder.getDrawable("ic_fso_folder_drawable"); //$NON-NLS-1$ - } else if (history.getItem() instanceof SearchInfoParcelable) { - this.mData[i].mDwIcon = - this.mIconHolder.getDrawable("ic_history_search_drawable"); //$NON-NLS-1$ - } - this.mData[i].mName = history.getItem().getTitle(); - if (this.mData[i].mName == null || this.mData[i].mName.trim().length() == 0) { - // Root directory - this.mData[i].mName = getContext().getString(R.string.root_directory_name); - } - this.mData[i].mDirectory = history.getItem().getDescription(); - this.mData[i].mPosition = String.format("#%d", Integer.valueOf(i + 1)); //$NON-NLS-1$ - } - } - - /** - * {@inheritDoc} - */ - @Override - public View getView(int position, View convertView, ViewGroup parent) { - - //Check to reuse view - View v = convertView; - if (v == null) { - //Create the view holder - LayoutInflater li = - (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - v = li.inflate(RESOURCE_LAYOUT, parent, false); - ViewHolder viewHolder = new HistoryAdapter.ViewHolder(); - viewHolder.mIvIcon = (ImageView)v.findViewById(RESOURCE_ITEM_ICON); - viewHolder.mTvName = (TextView)v.findViewById(RESOURCE_ITEM_NAME); - viewHolder.mTvDirectory = (TextView)v.findViewById(RESOURCE_ITEM_DIRECTORY); - viewHolder.mTvPosition = (TextView)v.findViewById(RESOURCE_ITEM_POSITION); - v.setTag(viewHolder); - - // Apply the current theme - Theme theme = ThemeManager.getCurrentTheme(getContext()); - theme.setBackgroundDrawable( - getContext(), v, "selectors_deselected_drawable"); //$NON-NLS-1$ - theme.setTextColor( - getContext(), viewHolder.mTvName, "text_color"); //$NON-NLS-1$ - theme.setTextColor( - getContext(), viewHolder.mTvDirectory, "text_color"); //$NON-NLS-1$ - theme.setTextColor( - getContext(), viewHolder.mTvPosition, "text_color"); //$NON-NLS-1$ - } - - //Retrieve data holder - final DataHolder dataHolder = this.mData[position]; - - //Retrieve the view holder - ViewHolder viewHolder = (ViewHolder)v.getTag(); - - //Set the data - viewHolder.mIvIcon.setImageDrawable(dataHolder.mDwIcon); - viewHolder.mTvName.setText(dataHolder.mName); - viewHolder.mTvDirectory.setText(dataHolder.mDirectory); - viewHolder.mTvPosition.setText(dataHolder.mPosition); - - //Return the view - return v; - } - - /** - * Method that should be invoked when the theme of the app was changed - */ - public void notifyThemeChanged() { - if (mIconHolder != null) { - mIconHolder.cleanup(); - } - // Empty icon holder (only have folders and search icons) - this.mIconHolder = new IconHolder(getContext(), false); - } - -} diff --git a/src/com/cyanogenmod/filemanager/ui/ThemeManager.java b/src/com/cyanogenmod/filemanager/ui/ThemeManager.java index 802ad1e5..65f264f2 100644 --- a/src/com/cyanogenmod/filemanager/ui/ThemeManager.java +++ b/src/com/cyanogenmod/filemanager/ui/ThemeManager.java @@ -485,6 +485,25 @@ public final class ThemeManager { view.setImageDrawable(mDefaultTheme.mResources.getDrawable(id)); } + /** + * Method that returns the resource id of a drawable in the current theme + * + * @param ctx The current context + * @param resource The string resource + * @return int The resource id + */ + public int getResourceId(Context ctx, String resource) { + String resId = mId + "_" + resource; //$NON-NLS-1$ + int id = this.mResources.getIdentifier(resId, "drawable", this.mPackage); //$NON-NLS-1$ + if (id != 0) { + return id; + } + + // Default theme + return mDefaultTheme.mResources.getIdentifier( + resource, "drawable", mDefaultTheme.mPackage); //$NON-NLS-1$ + } + /** * Method that returns an image drawable of the current theme * diff --git a/src/com/cyanogenmod/filemanager/ui/dialogs/ActionsDialog.java b/src/com/cyanogenmod/filemanager/ui/dialogs/ActionsDialog.java index 8c205434..bb34dceb 100644 --- a/src/com/cyanogenmod/filemanager/ui/dialogs/ActionsDialog.java +++ b/src/com/cyanogenmod/filemanager/ui/dialogs/ActionsDialog.java @@ -34,9 +34,11 @@ import android.widget.Toast; import com.cyanogenmod.filemanager.FileManagerApplication; import com.cyanogenmod.filemanager.R; +import com.cyanogenmod.filemanager.activities.NavigationActivity; import com.cyanogenmod.filemanager.adapters.TwoColumnsMenuListAdapter; import com.cyanogenmod.filemanager.listeners.OnRequestRefreshListener; import com.cyanogenmod.filemanager.listeners.OnSelectionListener; +import com.cyanogenmod.filemanager.model.Bookmark; import com.cyanogenmod.filemanager.model.FileSystemObject; import com.cyanogenmod.filemanager.model.Symlink; import com.cyanogenmod.filemanager.model.SystemFile; @@ -74,6 +76,7 @@ public class ActionsDialog implements OnItemClickListener, OnItemLongClickListen * @hide */ final Context mContext; + final NavigationActivity mBackRef; private final boolean mGlobal; private final boolean mSearch; private final boolean mChRooted; @@ -105,12 +108,14 @@ public class ActionsDialog implements OnItemClickListener, OnItemLongClickListen * @param global If the menu to display will be the global one (Global actions) * @param search If the call is from search activity */ - public ActionsDialog(Context context, FileSystemObject fso, boolean global, boolean search) { + public ActionsDialog(Context context, NavigationActivity backRef, FileSystemObject fso, + boolean global, boolean search) { super(); //Save the data this.mFso = fso; this.mContext = context; + this.mBackRef = backRef; this.mGlobal = global; this.mSearch = search; this.mChRooted = FileManagerApplication.getAccessMode().compareTo(AccessMode.SAFE) == 0; @@ -386,7 +391,12 @@ public class ActionsDialog implements OnItemClickListener, OnItemLongClickListen //- Add to bookmarks case R.id.mnu_actions_add_to_bookmarks: case R.id.mnu_actions_add_to_bookmarks_current_folder: - BookmarksActionPolicy.addToBookmarks(this.mContext, this.mFso); + Bookmark bookmark = BookmarksActionPolicy.addToBookmarks( + this.mContext, this.mFso); + if (mBackRef != null) { + // tell NavigationActivity's drawer to add the bookmark + mBackRef.addBookmark(bookmark); + } break; //- Add shortcut diff --git a/src/com/cyanogenmod/filemanager/ui/policy/BookmarksActionPolicy.java b/src/com/cyanogenmod/filemanager/ui/policy/BookmarksActionPolicy.java index bc1d39f6..ce3fe2e6 100644 --- a/src/com/cyanogenmod/filemanager/ui/policy/BookmarksActionPolicy.java +++ b/src/com/cyanogenmod/filemanager/ui/policy/BookmarksActionPolicy.java @@ -38,7 +38,7 @@ public final class BookmarksActionPolicy extends ActionsPolicy { * @param ctx The current context * @param fso The file system object */ - public static void addToBookmarks(final Context ctx, final FileSystemObject fso) { + public static Bookmark addToBookmarks(final Context ctx, final FileSystemObject fso) { try { // Create the bookmark Bookmark bookmark = @@ -50,17 +50,20 @@ public final class BookmarksActionPolicy extends ActionsPolicy { ctx, R.string.msgs_operation_failure, Toast.LENGTH_SHORT); - } else { - // Success - DialogHelper.showToast( - ctx, - R.string.bookmarks_msgs_add_success, - Toast.LENGTH_SHORT); + return null; } + // Success + DialogHelper.showToast( + ctx, + R.string.bookmarks_msgs_add_success, + Toast.LENGTH_SHORT); + return bookmark; + } catch (Exception e) { ExceptionUtil.translateException(ctx, e); } + return null; } } \ No newline at end of file diff --git a/src/com/cyanogenmod/filemanager/ui/widgets/ActionBarDrawerToggle.java b/src/com/cyanogenmod/filemanager/ui/widgets/ActionBarDrawerToggle.java new file mode 100644 index 00000000..4ef9e483 --- /dev/null +++ b/src/com/cyanogenmod/filemanager/ui/widgets/ActionBarDrawerToggle.java @@ -0,0 +1,554 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * (modified from android.support.v4.app) + */ + +package com.cyanogenmod.filemanager.ui.widgets; + +import java.lang.reflect.Method; + +import android.R; +import android.app.ActionBar; +import android.app.Activity; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LevelListDrawable; +import android.os.Build; +import android.util.Log; +import android.view.Gravity; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +/** + * This class provides a handy way to tie together the functionality of + * {@link DrawerLayout} and the framework ActionBar to implement + * the recommended design for navigation drawers. + * + *

+ * To use ActionBarDrawerToggle, create one in your Activity and + * call through to the following methods corresponding to your Activity + * callbacks: + *

+ * + *
    + *
  • + * {@link Activity#onConfigurationChanged(android.content.res.Configuration) + * onConfigurationChanged}
  • + *
  • {@link Activity#onOptionsItemSelected(android.view.MenuItem) + * onOptionsItemSelected}
  • + *
+ * + *

+ * Call {@link #syncState()} from your Activity's + * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} to synchronize + * the indicator with the state of the linked DrawerLayout after + * onRestoreInstanceState has occurred. + *

+ * + *

+ * ActionBarDrawerToggle can be used directly as a + * {@link DrawerLayout.DrawerListener}, or if you are already providing your own + * listener, call through to each of the listener methods from your own. + *

+ */ +public class ActionBarDrawerToggle implements DrawerLayout.DrawerListener { + private static final String TAG = "ActionBarDrawerToggle"; + + /** + * Allows an implementing Activity to return an + * {@link ActionBarDrawerToggle.Delegate} to use with ActionBarDrawerToggle. + */ + public interface DelegateProvider { + + /** + * @return Delegate to use for ActionBarDrawableToggles, or null if the + * Activity does not wish to override the default behavior. + */ + Delegate getDrawerToggleDelegate(); + } + + public interface Delegate { + /** + * @return Up indicator drawable as defined in the Activity's theme, or + * null if one is not defined. + */ + Drawable getThemeUpIndicator(); + + /** + * Set the Action Bar's up indicator drawable and content description. + * + * @param upDrawable + * - Drawable to set as up indicator + * @param contentDescRes + * - Content description to set + */ + void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes); + + /** + * Set the Action Bar's up indicator content description. + * + * @param contentDescRes + * - Content description to set + */ + void setActionBarDescription(int contentDescRes); + } + + private static final int[] THEME_ATTRS = new int[] { R.attr.homeAsUpIndicator }; + + private static class ActionBarDrawerToggleImpl { + public static Drawable getThemeUpIndicator(Activity activity) { + final TypedArray a = activity.obtainStyledAttributes(THEME_ATTRS); + final Drawable result = a.getDrawable(0); + a.recycle(); + return result; + } + + public static Object setActionBarUpIndicator(Object info, + Activity activity, Drawable drawable, int contentDescRes) { + if (info == null) { + info = new SetIndicatorInfo(activity); + } + + final ActionBar actionBar = activity.getActionBar(); + actionBar.setHomeAsUpIndicator(drawable); + actionBar.setHomeActionContentDescription(contentDescRes); + + return info; + } + + public static Object setActionBarDescription(Object info, + Activity activity, int contentDescRes) { + if (info == null) { + info = new SetIndicatorInfo(activity); + } + + final ActionBar actionBar = activity.getActionBar(); + actionBar.setHomeActionContentDescription(contentDescRes); + + return info; + } + } + + private static class SetIndicatorInfo { + public Method setHomeAsUpIndicator; + public Method setHomeActionContentDescription; + public ImageView upIndicatorView; + + SetIndicatorInfo(Activity activity) { + try { + setHomeAsUpIndicator = ActionBar.class.getDeclaredMethod( + "setHomeAsUpIndicator", Drawable.class); + setHomeActionContentDescription = ActionBar.class + .getDeclaredMethod("setHomeActionContentDescription", + Integer.TYPE); + + // If we got the method we won't need the stuff below. + return; + } catch (NoSuchMethodException e) { + // Oh well. We'll use the other mechanism below instead. + } + + final View home = activity.findViewById(android.R.id.home); + if (home == null) { + // Action bar doesn't have a known configuration, an OEM messed + // with things. + return; + } + + final ViewGroup parent = (ViewGroup) home.getParent(); + final int childCount = parent.getChildCount(); + if (childCount != 2) { + // No idea which one will be the right one, an OEM messed with + // things. + return; + } + + final View first = parent.getChildAt(0); + final View second = parent.getChildAt(1); + final View up = first.getId() == android.R.id.home ? second : first; + + if (up instanceof ImageView) { + // Jackpot! (Probably...) + upIndicatorView = (ImageView) up; + } + } + } + + private static final ActionBarDrawerToggleImpl IMPL = new ActionBarDrawerToggleImpl(); + + /** Fraction of its total width by which to offset the toggle drawable. */ + private static final float TOGGLE_DRAWABLE_OFFSET = 1 / 3f; + + // android.R.id.home as defined by public API in v11 + private static final int ID_HOME = 0x0102002c; + + private final Activity mActivity; + private final Delegate mActivityImpl; + private final DrawerLayout mDrawerLayout; + private boolean mDrawerIndicatorEnabled = true; + + private Drawable mThemeImage; + private Drawable mDrawerImage; + private SlideDrawable mSlider; + private int mDrawerImageResource; + private final int mOpenDrawerContentDescRes; + private final int mCloseDrawerContentDescRes; + + private Object mSetIndicatorInfo; + + /** + * Construct a new ActionBarDrawerToggle. + * + *

+ * The given {@link Activity} will be linked to the specified + * {@link DrawerLayout}. The provided drawer indicator drawable will animate + * slightly off-screen as the drawer is opened, indicating that in the open + * state the drawer will move off-screen when pressed and in the closed + * state the drawer will move on-screen when pressed. + *

+ * + *

+ * String resources must be provided to describe the open/close drawer + * actions for accessibility services. + *

+ * + * @param activity + * The Activity hosting the drawer + * @param drawerLayout + * The DrawerLayout to link to the given Activity's ActionBar + * @param drawerImageRes + * A Drawable resource to use as the drawer indicator + * @param openDrawerContentDescRes + * A String resource to describe the "open drawer" action for + * accessibility + * @param closeDrawerContentDescRes + * A String resource to describe the "close drawer" action for + * accessibility + */ + public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, + int drawerImageRes, int openDrawerContentDescRes, + int closeDrawerContentDescRes) { + mActivity = activity; + + // Allow the Activity to provide an impl + if (activity instanceof DelegateProvider) { + mActivityImpl = ((DelegateProvider) activity) + .getDrawerToggleDelegate(); + } else { + mActivityImpl = null; + } + + mDrawerLayout = drawerLayout; + mDrawerImageResource = drawerImageRes; + mOpenDrawerContentDescRes = openDrawerContentDescRes; + mCloseDrawerContentDescRes = closeDrawerContentDescRes; + + mThemeImage = getThemeUpIndicator(); + mDrawerImage = activity.getResources().getDrawable(drawerImageRes); + mSlider = new SlideDrawable(mDrawerImage); + mSlider.setOffset(TOGGLE_DRAWABLE_OFFSET); + } + + /** + * Synchronize the state of the drawer indicator/affordance with the linked + * DrawerLayout. + * + *

+ * This should be called from your Activity's + * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} method to + * synchronize after the DrawerLayout's instance state has been restored, + * and any other time when the state may have diverged in such a way that + * the ActionBarDrawerToggle was not notified. (For example, if you stop + * forwarding appropriate drawer events for a period of time.) + *

+ */ + public void syncState() { + if (mDrawerLayout.isDrawerOpen(Gravity.START)) { + mSlider.setPosition(1); + } else { + mSlider.setPosition(0); + } + + if (mDrawerIndicatorEnabled) { + setActionBarUpIndicator( + mSlider, + mDrawerLayout.isDrawerOpen(Gravity.START) ? mCloseDrawerContentDescRes + : mOpenDrawerContentDescRes); + } + } + + /** + * Enable or disable the drawer indicator. The indicator defaults to + * enabled. + * + *

+ * When the indicator is disabled, the ActionBar will revert to + * displaying the home-as-up indicator provided by the Activity + * 's theme in the android.R.attr.homeAsUpIndicator attribute + * instead of the animated drawer glyph. + *

+ * + * @param enable + * true to enable, false to disable + */ + public void setDrawerIndicatorEnabled(boolean enable) { + if (enable != mDrawerIndicatorEnabled) { + if (enable) { + setActionBarUpIndicator( + mSlider, + mDrawerLayout.isDrawerOpen(Gravity.START) ? mCloseDrawerContentDescRes + : mOpenDrawerContentDescRes); + } else { + setActionBarUpIndicator(mThemeImage, 0); + } + mDrawerIndicatorEnabled = enable; + } + } + + /** + * @return true if the enhanced drawer indicator is enabled, false otherwise + * @see #setDrawerIndicatorEnabled(boolean) + */ + public boolean isDrawerIndicatorEnabled() { + return mDrawerIndicatorEnabled; + } + + /** + * This method replaces the drawer image resource with a new one. + * + * @param newDrawerImageRes + * The new resource id + */ + public void setDrawerImageResource(int newDrawerImageRes) { + mDrawerImageResource = newDrawerImageRes; + mDrawerImage = mActivity.getResources().getDrawable( + mDrawerImageResource); + mSlider = new SlideDrawable(mDrawerImage); + mSlider.setOffset(TOGGLE_DRAWABLE_OFFSET); + syncState(); + } + + /** + * This method should always be called by your Activity's + * {@link Activity#onConfigurationChanged(android.content.res.Configuration) + * onConfigurationChanged} method. + * + * @param newConfig + * The new configuration + */ + public void onConfigurationChanged(Configuration newConfig) { + // Reload drawables that can change with configuration + mThemeImage = getThemeUpIndicator(); + mDrawerImage = mActivity.getResources().getDrawable( + mDrawerImageResource); + syncState(); + } + + /** + * This method should be called by your Activity's + * {@link Activity#onOptionsItemSelected(android.view.MenuItem) + * onOptionsItemSelected} method. If it returns true, your + * onOptionsItemSelected method should return true and skip + * further processing. + * + * @param item + * the MenuItem instance representing the selected menu item + * @return true if the event was handled and further processing should not + * occur + */ + public boolean onOptionsItemSelected(MenuItem item) { + if (item != null && item.getItemId() == ID_HOME + && mDrawerIndicatorEnabled) { + if (mDrawerLayout.isDrawerVisible(Gravity.START)) { + mDrawerLayout.closeDrawer(Gravity.START); + } else { + mDrawerLayout.openDrawer(Gravity.START); + } + return true; + } + return false; + } + + /** + * {@link DrawerLayout.DrawerListener} callback method. If you do not use + * your ActionBarDrawerToggle instance directly as your DrawerLayout's + * listener, you should call through to this method from your own listener + * object. + * + * @param drawerView + * The child view that was moved + * @param slideOffset + * The new offset of this drawer within its range, from 0-1 + */ + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + float glyphOffset = mSlider.getPosition(); + if (slideOffset > 0.5f) { + glyphOffset = Math.max(glyphOffset, + Math.max(0.f, slideOffset - 0.5f) * 2); + } else { + glyphOffset = Math.min(glyphOffset, slideOffset * 2); + } + mSlider.setPosition(glyphOffset); + } + + /** + * {@link DrawerLayout.DrawerListener} callback method. If you do not use + * your ActionBarDrawerToggle instance directly as your DrawerLayout's + * listener, you should call through to this method from your own listener + * object. + * + * @param drawerView + * Drawer view that is now open + */ + @Override + public void onDrawerOpened(View drawerView) { + mSlider.setPosition(1); + if (mDrawerIndicatorEnabled) { + setActionBarDescription(mCloseDrawerContentDescRes); + } + } + + /** + * {@link DrawerLayout.DrawerListener} callback method. If you do not use + * your ActionBarDrawerToggle instance directly as your DrawerLayout's + * listener, you should call through to this method from your own listener + * object. + * + * @param drawerView + * Drawer view that is now closed + */ + @Override + public void onDrawerClosed(View drawerView) { + mSlider.setPosition(0); + if (mDrawerIndicatorEnabled) { + setActionBarDescription(mOpenDrawerContentDescRes); + } + } + + /** + * {@link DrawerLayout.DrawerListener} callback method. If you do not use + * your ActionBarDrawerToggle instance directly as your DrawerLayout's + * listener, you should call through to this method from your own listener + * object. + * + * @param newState + * The new drawer motion state + */ + @Override + public void onDrawerStateChanged(int newState) { + } + + Drawable getThemeUpIndicator() { + if (mActivityImpl != null) { + return mActivityImpl.getThemeUpIndicator(); + } + return IMPL.getThemeUpIndicator(mActivity); + } + + void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) { + if (mActivityImpl != null) { + mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes); + return; + } + mSetIndicatorInfo = IMPL.setActionBarUpIndicator(mSetIndicatorInfo, + mActivity, upDrawable, contentDescRes); + } + + void setActionBarDescription(int contentDescRes) { + if (mActivityImpl != null) { + mActivityImpl.setActionBarDescription(contentDescRes); + return; + } + mSetIndicatorInfo = IMPL.setActionBarDescription(mSetIndicatorInfo, + mActivity, contentDescRes); + } + + private class SlideDrawable extends LevelListDrawable implements + Drawable.Callback { + private final boolean mHasMirroring = Build.VERSION.SDK_INT > 18; + private final Rect mTmpRect = new Rect(); + + private float mPosition; + private float mOffset; + + private SlideDrawable(Drawable wrapped) { + super(); + + if (wrapped.isAutoMirrored()) { + this.setAutoMirrored(true); + } + + addLevel(0, 0, wrapped); + } + + /** + * Sets the current position along the offset. + * + * @param position + * a value between 0 and 1 + */ + public void setPosition(float position) { + mPosition = position; + invalidateSelf(); + } + + public float getPosition() { + return mPosition; + } + + /** + * Specifies the maximum offset when the position is at 1. + * + * @param offset + * maximum offset as a fraction of the drawable width, + * positive to shift left or negative to shift right. + * @see #setPosition(float) + */ + public void setOffset(float offset) { + mOffset = offset; + invalidateSelf(); + } + + @Override + public void draw(Canvas canvas) { + copyBounds(mTmpRect); + canvas.save(); + + // Layout direction must be obtained from the activity. + final boolean isLayoutRTL = mActivity.getWindow().getDecorView() + .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + final int flipRtl = isLayoutRTL ? -1 : 1; + final int width = mTmpRect.width(); + canvas.translate(-mOffset * width * mPosition * flipRtl, 0); + + // Force auto-mirroring if it's not supported by the platform. + if (isLayoutRTL && !mHasMirroring) { + canvas.translate(width, 0); + canvas.scale(-1, 1); + } + + super.draw(canvas); + canvas.restore(); + } + } +} diff --git a/src/com/cyanogenmod/filemanager/ui/widgets/DrawerLayout.java b/src/com/cyanogenmod/filemanager/ui/widgets/DrawerLayout.java new file mode 100644 index 00000000..66c0aedc --- /dev/null +++ b/src/com/cyanogenmod/filemanager/ui/widgets/DrawerLayout.java @@ -0,0 +1,1621 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * (modified from android.support.v4.widget) + */ + + +package com.cyanogenmod.filemanager.ui.widgets; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +/** + * DrawerLayout acts as a top-level container for window content that allows for + * interactive "drawer" views to be pulled out from the edge of the window. + * + *

Drawer positioning and layout is controlled using the android:layout_gravity + * attribute on child views corresponding to which side of the view you want the drawer + * to emerge from: left or right. (Or start/end on platform versions that support layout direction.) + *

+ * + *

To use a DrawerLayout, position your primary content view as the first child with + * a width and height of match_parent. Add drawers as child views after the main + * content view and set the layout_gravity appropriately. Drawers commonly use + * match_parent for height with a fixed width.

+ * + *

{@link DrawerListener} can be used to monitor the state and motion of drawer views. + * Avoid performing expensive operations such as layout during animation as it can cause + * stuttering; try to perform expensive operations during the {@link #STATE_IDLE} state. + * {@link SimpleDrawerListener} offers default/no-op implementations of each callback method.

+ * + *

As per the Android Design guide, any drawers positioned to the left/start should + * always contain content for navigating around the application, whereas any drawers + * positioned to the right/end should always contain actions to take on the current content. + * This preserves the same navigation left, actions right structure present in the Action Bar + * and elsewhere.

+ */ +public class DrawerLayout extends ViewGroup { + private static final String TAG = "DrawerLayout"; + + /** + * Indicates that any drawers are in an idle, settled state. No animation is in progress. + */ + public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE; + + /** + * Indicates that a drawer is currently being dragged by the user. + */ + public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; + + /** + * Indicates that a drawer is in the process of settling to a final position. + */ + public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; + + /** + * The drawer is unlocked. + */ + public static final int LOCK_MODE_UNLOCKED = 0; + + /** + * The drawer is locked closed. The user may not open it, though + * the app may open it programmatically. + */ + public static final int LOCK_MODE_LOCKED_CLOSED = 1; + + /** + * The drawer is locked open. The user may not close it, though the app + * may close it programmatically. + */ + public static final int LOCK_MODE_LOCKED_OPEN = 2; + + private static final int MIN_DRAWER_MARGIN = 64; // dp + + private static final int DEFAULT_SCRIM_COLOR = 0x99000000; + + /** + * Length of time to delay before peeking the drawer. + */ + private static final int PEEK_DELAY = 160; // ms + + /** + * Minimum velocity that will be detected as a fling + */ + private static final int MIN_FLING_VELOCITY = 400; // dips per second + + /** + * Experimental feature. + */ + private static final boolean ALLOW_EDGE_LOCK = false; + + private static final boolean CHILDREN_DISALLOW_INTERCEPT = true; + + private static final float TOUCH_SLOP_SENSITIVITY = 1.f; + + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.layout_gravity + }; + + private int mMinDrawerMargin; + + private int mScrimColor = DEFAULT_SCRIM_COLOR; + private float mScrimOpacity; + private Paint mScrimPaint = new Paint(); + + private final ViewDragHelper mLeftDragger; + private final ViewDragHelper mRightDragger; + private final ViewDragCallback mLeftCallback; + private final ViewDragCallback mRightCallback; + private int mDrawerState; + private boolean mInLayout; + private boolean mFirstLayout = true; + private int mLockModeLeft; + private int mLockModeRight; + @SuppressWarnings("unused") + private boolean mDisallowInterceptRequested; + private boolean mChildrenCanceledTouch; + + private DrawerListener mListener; + + private float mInitialMotionX; + private float mInitialMotionY; + + private Drawable mShadowLeft; + private Drawable mShadowRight; + + /** + * Listener for monitoring events about drawers. + */ + public interface DrawerListener { + /** + * Called when a drawer's position changes. + * @param drawerView The child view that was moved + * @param slideOffset The new offset of this drawer within its range, from 0-1 + */ + public void onDrawerSlide(View drawerView, float slideOffset); + + /** + * Called when a drawer has settled in a completely open state. + * The drawer is interactive at this point. + * + * @param drawerView Drawer view that is now open + */ + public void onDrawerOpened(View drawerView); + + /** + * Called when a drawer has settled in a completely closed state. + * + * @param drawerView Drawer view that is now closed + */ + public void onDrawerClosed(View drawerView); + + /** + * Called when the drawer motion state changes. The new state will + * be one of {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. + * + * @param newState The new drawer motion state + */ + public void onDrawerStateChanged(int newState); + } + + /** + * Stub/no-op implementations of all methods of {@link DrawerListener}. + * Override this if you only care about a few of the available callback methods. + */ + public static abstract class SimpleDrawerListener implements DrawerListener { + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + } + + @Override + public void onDrawerOpened(View drawerView) { + } + + @Override + public void onDrawerClosed(View drawerView) { + } + + @Override + public void onDrawerStateChanged(int newState) { + } + } + + public DrawerLayout(Context context) { + this(context, null); + } + + public DrawerLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DrawerLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final float density = getResources().getDisplayMetrics().density; + mMinDrawerMargin = (int) (MIN_DRAWER_MARGIN * density + 0.5f); + final float minVel = MIN_FLING_VELOCITY * density; + + mLeftCallback = new ViewDragCallback(Gravity.LEFT); + mRightCallback = new ViewDragCallback(Gravity.RIGHT); + + mLeftDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mLeftCallback); + mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); + mLeftDragger.setMinVelocity(minVel); + mLeftCallback.setDragger(mLeftDragger); + + mRightDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mRightCallback); + mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT); + mRightDragger.setMinVelocity(minVel); + mRightCallback.setDragger(mRightDragger); + + // So that we can catch the back button + setFocusableInTouchMode(true); + + this.setAccessibilityDelegate(new AccessibilityDelegate()); + this.setMotionEventSplittingEnabled(false); + } + + /** + * Set a simple drawable used for the left or right shadow. + * The drawable provided must have a nonzero intrinsic width. + * + * @param shadowDrawable Shadow drawable to use at the edge of a drawer + * @param gravity Which drawer the shadow should apply to + */ + public void setDrawerShadow(Drawable shadowDrawable, int gravity) { + /* + * TODO Someone someday might want to set more complex drawables here. + * They're probably nuts, but we might want to consider registering callbacks, + * setting states, etc. properly. + */ + + final int absGravity = Gravity.getAbsoluteGravity(gravity, + this.getLayoutDirection()); + if ((absGravity & Gravity.LEFT) == Gravity.LEFT) { + mShadowLeft = shadowDrawable; + invalidate(); + } + if ((absGravity & Gravity.RIGHT) == Gravity.RIGHT) { + mShadowRight = shadowDrawable; + invalidate(); + } + } + + /** + * Set a simple drawable used for the left or right shadow. + * The drawable provided must have a nonzero intrinsic width. + * + * @param resId Resource id of a shadow drawable to use at the edge of a drawer + * @param gravity Which drawer the shadow should apply to + */ + public void setDrawerShadow(int resId, int gravity) { + setDrawerShadow(getResources().getDrawable(resId), gravity); + } + + /** + * Set a color to use for the scrim that obscures primary content while a drawer is open. + * + * @param color Color to use in 0xAARRGGBB format. + */ + public void setScrimColor(int color) { + mScrimColor = color; + invalidate(); + } + + /** + * Set a listener to be notified of drawer events. + * + * @param listener Listener to notify when drawer events occur + * @see DrawerListener + */ + public void setDrawerListener(DrawerListener listener) { + mListener = listener; + } + + /** + * Enable or disable interaction with all drawers. + * + *

This allows the application to restrict the user's ability to open or close + * any drawer within this layout. DrawerLayout will still respond to calls to + * {@link #openDrawer(int)}, {@link #closeDrawer(int)} and friends if a drawer is locked.

+ * + *

Locking drawers open or closed will implicitly open or close + * any drawers as appropriate.

+ * + * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, + * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. + */ + public void setDrawerLockMode(int lockMode) { + setDrawerLockMode(lockMode, Gravity.LEFT); + setDrawerLockMode(lockMode, Gravity.RIGHT); + } + + /** + * Enable or disable interaction with the given drawer. + * + *

This allows the application to restrict the user's ability to open or close + * the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer(int)}, + * {@link #closeDrawer(int)} and friends if a drawer is locked.

+ * + *

Locking a drawer open or closed will implicitly open or close + * that drawer as appropriate.

+ * + * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, + * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. + * @param edgeGravity Gravity.LEFT, RIGHT, START or END. + * Expresses which drawer to change the mode for. + * + * @see #LOCK_MODE_UNLOCKED + * @see #LOCK_MODE_LOCKED_CLOSED + * @see #LOCK_MODE_LOCKED_OPEN + */ + public void setDrawerLockMode(int lockMode, int edgeGravity) { + final int absGravity = Gravity.getAbsoluteGravity(edgeGravity, + this.getLayoutDirection()); + if (absGravity == Gravity.LEFT) { + mLockModeLeft = lockMode; + } else if (absGravity == Gravity.RIGHT) { + mLockModeRight = lockMode; + } + if (lockMode != LOCK_MODE_UNLOCKED) { + // Cancel interaction in progress + final ViewDragHelper helper = absGravity == Gravity.LEFT ? mLeftDragger : mRightDragger; + helper.cancel(); + } + switch (lockMode) { + case LOCK_MODE_LOCKED_OPEN: + final View toOpen = findDrawerWithGravity(absGravity); + if (toOpen != null) { + openDrawer(toOpen); + } + break; + case LOCK_MODE_LOCKED_CLOSED: + final View toClose = findDrawerWithGravity(absGravity); + if (toClose != null) { + closeDrawer(toClose); + } + break; + // default: do nothing + } + } + + /** + * Enable or disable interaction with the given drawer. + * + *

This allows the application to restrict the user's ability to open or close + * the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer(int)}, + * {@link #closeDrawer(int)} and friends if a drawer is locked.

+ * + *

Locking a drawer open or closed will implicitly open or close + * that drawer as appropriate.

+ * + * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, + * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. + * @param drawerView The drawer view to change the lock mode for + * + * @see #LOCK_MODE_UNLOCKED + * @see #LOCK_MODE_LOCKED_CLOSED + * @see #LOCK_MODE_LOCKED_OPEN + */ + public void setDrawerLockMode(int lockMode, View drawerView) { + if (!isDrawerView(drawerView)) { + throw new IllegalArgumentException("View " + drawerView + " is not a " + + "drawer with appropriate layout_gravity"); + } + final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; + setDrawerLockMode(lockMode, gravity); + } + + /** + * Check the lock mode of the drawer with the given gravity. + * + * @param edgeGravity Gravity of the drawer to check + * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or + * {@link #LOCK_MODE_LOCKED_OPEN}. + */ + public int getDrawerLockMode(int edgeGravity) { + final int absGravity = Gravity.getAbsoluteGravity( + edgeGravity, this.getLayoutDirection()); + if (absGravity == Gravity.LEFT) { + return mLockModeLeft; + } else if (absGravity == Gravity.RIGHT) { + return mLockModeRight; + } + return LOCK_MODE_UNLOCKED; + } + + /** + * Check the lock mode of the given drawer view. + * + * @param drawerView Drawer view to check lock mode + * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or + * {@link #LOCK_MODE_LOCKED_OPEN}. + */ + public int getDrawerLockMode(View drawerView) { + final int absGravity = getDrawerViewAbsoluteGravity(drawerView); + if (absGravity == Gravity.LEFT) { + return mLockModeLeft; + } else if (absGravity == Gravity.RIGHT) { + return mLockModeRight; + } + return LOCK_MODE_UNLOCKED; + } + + /** + * Resolve the shared state of all drawers from the component ViewDragHelpers. + * Should be called whenever a ViewDragHelper's state changes. + */ + void updateDrawerState(int forGravity, int activeState, View activeDrawer) { + final int leftState = mLeftDragger.getViewDragState(); + final int rightState = mRightDragger.getViewDragState(); + + final int state; + if (leftState == STATE_DRAGGING || rightState == STATE_DRAGGING) { + state = STATE_DRAGGING; + } else if (leftState == STATE_SETTLING || rightState == STATE_SETTLING) { + state = STATE_SETTLING; + } else { + state = STATE_IDLE; + } + + if (activeDrawer != null && activeState == STATE_IDLE) { + final LayoutParams lp = (LayoutParams) activeDrawer.getLayoutParams(); + if (lp.onScreen == 0) { + dispatchOnDrawerClosed(activeDrawer); + } else if (lp.onScreen == 1) { + dispatchOnDrawerOpened(activeDrawer); + } + } + + if (state != mDrawerState) { + mDrawerState = state; + + if (mListener != null) { + mListener.onDrawerStateChanged(state); + } + } + } + + void dispatchOnDrawerClosed(View drawerView) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + if (lp.knownOpen) { + lp.knownOpen = false; + if (mListener != null) { + mListener.onDrawerClosed(drawerView); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + } + + void dispatchOnDrawerOpened(View drawerView) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + if (!lp.knownOpen) { + lp.knownOpen = true; + if (mListener != null) { + mListener.onDrawerOpened(drawerView); + } + drawerView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + } + + void dispatchOnDrawerSlide(View drawerView, float slideOffset) { + if (mListener != null) { + mListener.onDrawerSlide(drawerView, slideOffset); + } + } + + void setDrawerViewOffset(View drawerView, float slideOffset) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + if (slideOffset == lp.onScreen) { + return; + } + + lp.onScreen = slideOffset; + dispatchOnDrawerSlide(drawerView, slideOffset); + } + + float getDrawerViewOffset(View drawerView) { + return ((LayoutParams) drawerView.getLayoutParams()).onScreen; + } + + /** + * @return the absolute gravity of the child drawerView, resolved according + * to the current layout direction + */ + int getDrawerViewAbsoluteGravity(View drawerView) { + final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; + return Gravity.getAbsoluteGravity(gravity, this.getLayoutDirection()); + } + + boolean checkDrawerViewAbsoluteGravity(View drawerView, int checkFor) { + final int absGravity = getDrawerViewAbsoluteGravity(drawerView); + return (absGravity & checkFor) == checkFor; + } + + View findOpenDrawer() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (((LayoutParams) child.getLayoutParams()).knownOpen) { + return child; + } + } + return null; + } + + void moveDrawerToOffset(View drawerView, float slideOffset) { + final float oldOffset = getDrawerViewOffset(drawerView); + final int width = drawerView.getWidth(); + final int oldPos = (int) (width * oldOffset); + final int newPos = (int) (width * slideOffset); + final int dx = newPos - oldPos; + + drawerView.offsetLeftAndRight( + checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT) ? dx : -dx); + setDrawerViewOffset(drawerView, slideOffset); + } + + /** + * @param gravity the gravity of the child to return. If specified as a + * relative value, it will be resolved according to the current + * layout direction. + * @return the drawer with the specified gravity + */ + View findDrawerWithGravity(int gravity) { + final int absHorizGravity = Gravity.getAbsoluteGravity( + gravity, this.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final int childAbsGravity = getDrawerViewAbsoluteGravity(child); + if ((childAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == absHorizGravity) { + return child; + } + } + return null; + } + + /** + * Simple gravity to string - only supports LEFT and RIGHT for debugging output. + * + * @param gravity Absolute gravity value + * @return LEFT or RIGHT as appropriate, or a hex string + */ + static String gravityToString(int gravity) { + if ((gravity & Gravity.LEFT) == Gravity.LEFT) { + return "LEFT"; + } + if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { + return "RIGHT"; + } + return Integer.toHexString(gravity); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mFirstLayout = true; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { + if (isInEditMode()) { + // Don't crash the layout editor. Consume all of the space if specified + // or pick a magic number from thin air otherwise. + // TODO Better communication with tools of this bogus state. + // It will crash on a real device. + if (widthMode == MeasureSpec.AT_MOST) { + widthMode = MeasureSpec.EXACTLY; + } else if (widthMode == MeasureSpec.UNSPECIFIED) { + widthMode = MeasureSpec.EXACTLY; + widthSize = 300; + } + if (heightMode == MeasureSpec.AT_MOST) { + heightMode = MeasureSpec.EXACTLY; + } + else if (heightMode == MeasureSpec.UNSPECIFIED) { + heightMode = MeasureSpec.EXACTLY; + heightSize = 300; + } + } else { + throw new IllegalArgumentException( + "DrawerLayout must be measured with MeasureSpec.EXACTLY."); + } + } + + setMeasuredDimension(widthSize, heightSize); + + // Gravity value for each drawer we've seen. Only one of each permitted. + int foundDrawers = 0; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (isContentView(child)) { + // Content views get measured at exactly the layout's size. + final int contentWidthSpec = MeasureSpec.makeMeasureSpec( + widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); + final int contentHeightSpec = MeasureSpec.makeMeasureSpec( + heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); + child.measure(contentWidthSpec, contentHeightSpec); + } else if (isDrawerView(child)) { + final int childGravity = + getDrawerViewAbsoluteGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK; + if ((foundDrawers & childGravity) != 0) { + throw new IllegalStateException("Child drawer has absolute gravity " + + gravityToString(childGravity) + " but this " + TAG + " already has a " + + "drawer view along that edge"); + } + final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, + mMinDrawerMargin + lp.leftMargin + lp.rightMargin, + lp.width); + final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec, + lp.topMargin + lp.bottomMargin, + lp.height); + child.measure(drawerWidthSpec, drawerHeightSpec); + } else { + throw new IllegalStateException("Child " + child + " at index " + i + + " does not have a valid layout_gravity - must be Gravity.LEFT, " + + "Gravity.RIGHT or Gravity.NO_GRAVITY"); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mInLayout = true; + final int width = r - l; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (isContentView(child)) { + child.layout(lp.leftMargin, lp.topMargin, + lp.leftMargin + child.getMeasuredWidth(), + lp.topMargin + child.getMeasuredHeight()); + } else { // Drawer, if it wasn't onMeasure would have thrown an exception. + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + int childLeft; + + final float newOffset; + if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + childLeft = -childWidth + (int) (childWidth * lp.onScreen); + newOffset = (float) (childWidth + childLeft) / childWidth; + } else { // Right; onMeasure checked for us. + childLeft = width - (int) (childWidth * lp.onScreen); + newOffset = (float) (width - childLeft) / childWidth; + } + + final boolean changeOffset = newOffset != lp.onScreen; + + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (vgrav) { + default: + case Gravity.TOP: { + child.layout(childLeft, lp.topMargin, childLeft + childWidth, + lp.topMargin + childHeight); + break; + } + + case Gravity.BOTTOM: { + final int height = b - t; + child.layout(childLeft, + height - lp.bottomMargin - child.getMeasuredHeight(), + childLeft + childWidth, + height - lp.bottomMargin); + break; + } + + case Gravity.CENTER_VERTICAL: { + final int height = b - t; + int childTop = (height - childHeight) / 2; + + // Offset for margins. If things don't fit right because of + // bad measurement before, oh well. + if (childTop < lp.topMargin) { + childTop = lp.topMargin; + } else if (childTop + childHeight > height - lp.bottomMargin) { + childTop = height - lp.bottomMargin - childHeight; + } + child.layout(childLeft, childTop, childLeft + childWidth, + childTop + childHeight); + break; + } + } + + if (changeOffset) { + setDrawerViewOffset(child, newOffset); + } + + final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE; + if (child.getVisibility() != newVisibility) { + child.setVisibility(newVisibility); + } + } + } + mInLayout = false; + mFirstLayout = false; + } + + @Override + public void requestLayout() { + if (!mInLayout) { + super.requestLayout(); + } + } + + @Override + public void computeScroll() { + final int childCount = getChildCount(); + float scrimOpacity = 0; + for (int i = 0; i < childCount; i++) { + final float onscreen = ((LayoutParams) getChildAt(i).getLayoutParams()).onScreen; + scrimOpacity = Math.max(scrimOpacity, onscreen); + } + mScrimOpacity = scrimOpacity; + + // "|" used on purpose; both need to run. + if (mLeftDragger.continueSettling(true) | mRightDragger.continueSettling(true)) { + this.postInvalidateOnAnimation(); + } + } + + private static boolean hasOpaqueBackground(View v) { + final Drawable bg = v.getBackground(); + if (bg != null) { + return bg.getOpacity() == PixelFormat.OPAQUE; + } + return false; + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + final int height = getHeight(); + final boolean drawingContent = isContentView(child); + int clipLeft = 0, clipRight = getWidth(); + + final int restoreCount = canvas.save(); + if (drawingContent) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + if (v == child || v.getVisibility() != VISIBLE || + !hasOpaqueBackground(v) || !isDrawerView(v) || + v.getHeight() < height) { + continue; + } + + if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) { + final int vright = v.getRight(); + if (vright > clipLeft) clipLeft = vright; + } else { + final int vleft = v.getLeft(); + if (vleft < clipRight) clipRight = vleft; + } + } + canvas.clipRect(clipLeft, 0, clipRight, getHeight()); + } + final boolean result = super.drawChild(canvas, child, drawingTime); + canvas.restoreToCount(restoreCount); + + if (mScrimOpacity > 0 && drawingContent) { + final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; + final int imag = (int) (baseAlpha * mScrimOpacity); + final int color = imag << 24 | (mScrimColor & 0xffffff); + mScrimPaint.setColor(color); + + canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint); + } else if (mShadowLeft != null && checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + final int shadowWidth = mShadowLeft.getIntrinsicWidth(); + final int childRight = child.getRight(); + final int drawerPeekDistance = mLeftDragger.getEdgeSize(); + final float alpha = + Math.max(0, Math.min((float) childRight / drawerPeekDistance, 1.f)); + mShadowLeft.setBounds(childRight, child.getTop(), + childRight + shadowWidth, child.getBottom()); + mShadowLeft.setAlpha((int) (0xff * alpha)); + mShadowLeft.draw(canvas); + } else if (mShadowRight != null && checkDrawerViewAbsoluteGravity(child, Gravity.RIGHT)) { + final int shadowWidth = mShadowRight.getIntrinsicWidth(); + final int childLeft = child.getLeft(); + final int showing = getWidth() - childLeft; + final int drawerPeekDistance = mRightDragger.getEdgeSize(); + final float alpha = + Math.max(0, Math.min((float) showing / drawerPeekDistance, 1.f)); + mShadowRight.setBounds(childLeft - shadowWidth, child.getTop(), + childLeft, child.getBottom()); + mShadowRight.setAlpha((int) (0xff * alpha)); + mShadowRight.draw(canvas); + } + return result; + } + + boolean isContentView(View child) { + return ((LayoutParams) child.getLayoutParams()).gravity == Gravity.NO_GRAVITY; + } + + boolean isDrawerView(View child) { + final int gravity = ((LayoutParams) child.getLayoutParams()).gravity; + final int absGravity = Gravity.getAbsoluteGravity(gravity, + child.getLayoutDirection()); + return (absGravity & (Gravity.LEFT | Gravity.RIGHT)) != 0; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = ev.getActionMasked(); + + // "|" used deliberately here; both methods should be invoked. + final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) | + mRightDragger.shouldInterceptTouchEvent(ev); + + boolean interceptForTap = false; + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + mInitialMotionX = x; + mInitialMotionY = y; + if (mScrimOpacity > 0 && + isContentView(mLeftDragger.findTopChildUnder((int) x, (int) y))) { + interceptForTap = true; + } + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + break; + } + + case MotionEvent.ACTION_MOVE: { + // If we cross the touch slop, don't perform the delayed peek for an edge touch. + if (mLeftDragger.checkTouchSlop(ViewDragHelper.DIRECTION_ALL)) { + mLeftCallback.removeCallbacks(); + mRightCallback.removeCallbacks(); + } + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + closeDrawers(true); + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + } + } + + return interceptForDrag || interceptForTap || hasPeekingDrawer() || mChildrenCanceledTouch; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mLeftDragger.processTouchEvent(ev); + mRightDragger.processTouchEvent(ev); + + final int action = ev.getAction(); + boolean wantTouchEvents = true; + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + mInitialMotionX = x; + mInitialMotionY = y; + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + break; + } + + case MotionEvent.ACTION_UP: { + final float x = ev.getX(); + final float y = ev.getY(); + boolean peekingOnly = true; + final View touchedView = mLeftDragger.findTopChildUnder((int) x, (int) y); + if (touchedView != null && isContentView(touchedView)) { + final float dx = x - mInitialMotionX; + final float dy = y - mInitialMotionY; + final int slop = mLeftDragger.getTouchSlop(); + if (dx * dx + dy * dy < slop * slop) { + // Taps close a dimmed open drawer but only if it isn't locked open. + final View openDrawer = findOpenDrawer(); + if (openDrawer != null) { + peekingOnly = getDrawerLockMode(openDrawer) == LOCK_MODE_LOCKED_OPEN; + } + } + } + closeDrawers(peekingOnly); + mDisallowInterceptRequested = false; + break; + } + + case MotionEvent.ACTION_CANCEL: { + closeDrawers(true); + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + break; + } + } + + return wantTouchEvents; + } + + @SuppressWarnings("unused") + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (CHILDREN_DISALLOW_INTERCEPT || + (!mLeftDragger.isEdgeTouched(ViewDragHelper.EDGE_LEFT) && + !mRightDragger.isEdgeTouched(ViewDragHelper.EDGE_RIGHT))) { + // If we have an edge touch we want to skip this and track it for later instead. + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + mDisallowInterceptRequested = disallowIntercept; + if (disallowIntercept) { + closeDrawers(true); + } + } + + /** + * Close all currently open drawer views by animating them out of view. + */ + public void closeDrawers() { + closeDrawers(false); + } + + void closeDrawers(boolean peekingOnly) { + boolean needsInvalidate = false; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (!isDrawerView(child) || (peekingOnly && !lp.isPeeking)) { + continue; + } + + final int childWidth = child.getWidth(); + + if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + needsInvalidate |= mLeftDragger.smoothSlideViewTo(child, + -childWidth, child.getTop()); + } else { + needsInvalidate |= mRightDragger.smoothSlideViewTo(child, + getWidth(), child.getTop()); + } + + lp.isPeeking = false; + } + + mLeftCallback.removeCallbacks(); + mRightCallback.removeCallbacks(); + + if (needsInvalidate) { + invalidate(); + } + } + + /** + * Open the specified drawer view by animating it into view. + * + * @param drawerView Drawer view to open + */ + public void openDrawer(View drawerView) { + if (!isDrawerView(drawerView)) { + throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); + } + + if (mFirstLayout) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + lp.onScreen = 1.f; + lp.knownOpen = true; + } else { + if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { + mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop()); + } else { + mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(), + drawerView.getTop()); + } + } + invalidate(); + } + + /** + * Open the specified drawer by animating it out of view. + * + * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. + * GravityCompat.START or GravityCompat.END may also be used. + */ + public void openDrawer(int gravity) { + final View drawerView = findDrawerWithGravity(gravity); + if (drawerView == null) { + throw new IllegalArgumentException("No drawer view found with gravity " + + gravityToString(gravity)); + } + openDrawer(drawerView); + } + + /** + * Close the specified drawer view by animating it into view. + * + * @param drawerView Drawer view to close + */ + public void closeDrawer(View drawerView) { + if (!isDrawerView(drawerView)) { + throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); + } + + if (mFirstLayout) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + lp.onScreen = 0.f; + lp.knownOpen = false; + } else { + if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { + mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(), + drawerView.getTop()); + } else { + mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop()); + } + } + invalidate(); + } + + /** + * Close the specified drawer by animating it out of view. + * + * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. + * GravityCompat.START or GravityCompat.END may also be used. + */ + public void closeDrawer(int gravity) { + final View drawerView = findDrawerWithGravity(gravity); + if (drawerView == null) { + throw new IllegalArgumentException("No drawer view found with gravity " + + gravityToString(gravity)); + } + closeDrawer(drawerView); + } + + /** + * Check if the given drawer view is currently in an open state. + * To be considered "open" the drawer must have settled into its fully + * visible state. To check for partial visibility use + * {@link #isDrawerVisible(android.view.View)}. + * + * @param drawer Drawer view to check + * @return true if the given drawer view is in an open state + * @see #isDrawerVisible(android.view.View) + */ + public boolean isDrawerOpen(View drawer) { + if (!isDrawerView(drawer)) { + throw new IllegalArgumentException("View " + drawer + " is not a drawer"); + } + return ((LayoutParams) drawer.getLayoutParams()).knownOpen; + } + + /** + * Check if the given drawer view is currently in an open state. + * To be considered "open" the drawer must have settled into its fully + * visible state. If there is no drawer with the given gravity this method + * will return false. + * + * @param drawerGravity Gravity of the drawer to check + * @return true if the given drawer view is in an open state + */ + public boolean isDrawerOpen(int drawerGravity) { + final View drawerView = findDrawerWithGravity(drawerGravity); + if (drawerView != null) { + return isDrawerOpen(drawerView); + } + return false; + } + + /** + * Check if a given drawer view is currently visible on-screen. The drawer + * may be only peeking onto the screen, fully extended, or anywhere inbetween. + * + * @param drawer Drawer view to check + * @return true if the given drawer is visible on-screen + * @see #isDrawerOpen(android.view.View) + */ + public boolean isDrawerVisible(View drawer) { + if (!isDrawerView(drawer)) { + throw new IllegalArgumentException("View " + drawer + " is not a drawer"); + } + return ((LayoutParams) drawer.getLayoutParams()).onScreen > 0; + } + + /** + * Check if a given drawer view is currently visible on-screen. The drawer + * may be only peeking onto the screen, fully extended, or anywhere inbetween. + * If there is no drawer with the given gravity this method will return false. + * + * @param drawerGravity Gravity of the drawer to check + * @return true if the given drawer is visible on-screen + */ + public boolean isDrawerVisible(int drawerGravity) { + final View drawerView = findDrawerWithGravity(drawerGravity); + if (drawerView != null) { + return isDrawerVisible(drawerView); + } + return false; + } + + private boolean hasPeekingDrawer() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); + if (lp.isPeeking) { + return true; + } + } + return false; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams + ? new LayoutParams((LayoutParams) p) + : p instanceof ViewGroup.MarginLayoutParams + ? new LayoutParams((MarginLayoutParams) p) + : new LayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + private boolean hasVisibleDrawer() { + return findVisibleDrawer() != null; + } + + private View findVisibleDrawer() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (isDrawerView(child) && isDrawerVisible(child)) { + return child; + } + } + return null; + } + + void cancelChildViewTouch() { + // Cancel child touches + if (!mChildrenCanceledTouch) { + final long now = SystemClock.uptimeMillis(); + final MotionEvent cancelEvent = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + getChildAt(i).dispatchTouchEvent(cancelEvent); + } + cancelEvent.recycle(); + mChildrenCanceledTouch = true; + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) { + event.startTracking(); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + final View visibleDrawer = findVisibleDrawer(); + if (visibleDrawer != null && getDrawerLockMode(visibleDrawer) == LOCK_MODE_UNLOCKED) { + closeDrawers(); + } + return visibleDrawer != null; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + final SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (ss.openDrawerGravity != Gravity.NO_GRAVITY) { + final View toOpen = findDrawerWithGravity(ss.openDrawerGravity); + if (toOpen != null) { + openDrawer(toOpen); + } + } + + setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT); + setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + + final SavedState ss = new SavedState(superState); + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (!isDrawerView(child)) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.knownOpen) { + ss.openDrawerGravity = lp.gravity; + // Only one drawer can be open at a time. + break; + } + } + + ss.lockModeLeft = mLockModeLeft; + ss.lockModeRight = mLockModeRight; + + return ss; + } + + /** + * State persisted across instances + */ + protected static class SavedState extends BaseSavedState { + int openDrawerGravity = Gravity.NO_GRAVITY; + int lockModeLeft = LOCK_MODE_UNLOCKED; + int lockModeRight = LOCK_MODE_UNLOCKED; + + public SavedState(Parcel in) { + super(in); + openDrawerGravity = in.readInt(); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(openDrawerGravity); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel source) { + return new SavedState(source); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + private class ViewDragCallback extends ViewDragHelper.Callback { + private final int mAbsGravity; + private ViewDragHelper mDragger; + + private final Runnable mPeekRunnable = new Runnable() { + @Override public void run() { + peekDrawer(); + } + }; + + public ViewDragCallback(int gravity) { + mAbsGravity = gravity; + } + + public void setDragger(ViewDragHelper dragger) { + mDragger = dragger; + } + + public void removeCallbacks() { + DrawerLayout.this.removeCallbacks(mPeekRunnable); + } + + @Override + public boolean tryCaptureView(View child, int pointerId) { + // Only capture views where the gravity matches what we're looking for. + // This lets us use two ViewDragHelpers, one for each side drawer. + return isDrawerView(child) && checkDrawerViewAbsoluteGravity(child, mAbsGravity) + && getDrawerLockMode(child) == LOCK_MODE_UNLOCKED; + } + + @Override + public void onViewDragStateChanged(int state) { + updateDrawerState(mAbsGravity, state, mDragger.getCapturedView()); + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + float offset; + final int childWidth = changedView.getWidth(); + + // This reverses the positioning shown in onLayout. + if (checkDrawerViewAbsoluteGravity(changedView, Gravity.LEFT)) { + offset = (float) (childWidth + left) / childWidth; + } else { + final int width = getWidth(); + offset = (float) (width - left) / childWidth; + } + setDrawerViewOffset(changedView, offset); + changedView.setVisibility(offset == 0 ? INVISIBLE : VISIBLE); + invalidate(); + } + + @Override + public void onViewCaptured(View capturedChild, int activePointerId) { + final LayoutParams lp = (LayoutParams) capturedChild.getLayoutParams(); + lp.isPeeking = false; + + closeOtherDrawer(); + } + + private void closeOtherDrawer() { + final int otherGrav = mAbsGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT; + final View toClose = findDrawerWithGravity(otherGrav); + if (toClose != null) { + closeDrawer(toClose); + } + } + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + // Offset is how open the drawer is, therefore left/right values + // are reversed from one another. + final float offset = getDrawerViewOffset(releasedChild); + final int childWidth = releasedChild.getWidth(); + + int left; + if (checkDrawerViewAbsoluteGravity(releasedChild, Gravity.LEFT)) { + left = xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -childWidth; + } else { + final int width = getWidth(); + left = xvel < 0 || xvel == 0 && offset > 0.5f ? width - childWidth : width; + } + + mDragger.settleCapturedViewAt(left, releasedChild.getTop()); + invalidate(); + } + + @Override + public void onEdgeTouched(int edgeFlags, int pointerId) { + postDelayed(mPeekRunnable, PEEK_DELAY); + } + + private void peekDrawer() { + final View toCapture; + final int childLeft; + final int peekDistance = mDragger.getEdgeSize(); + final boolean leftEdge = mAbsGravity == Gravity.LEFT; + if (leftEdge) { + toCapture = findDrawerWithGravity(Gravity.LEFT); + childLeft = (toCapture != null ? -toCapture.getWidth() : 0) + peekDistance; + } else { + toCapture = findDrawerWithGravity(Gravity.RIGHT); + childLeft = getWidth() - peekDistance; + } + // Only peek if it would mean making the drawer more visible and the drawer isn't locked + if (toCapture != null && ((leftEdge && toCapture.getLeft() < childLeft) || + (!leftEdge && toCapture.getLeft() > childLeft)) && + getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { + final LayoutParams lp = (LayoutParams) toCapture.getLayoutParams(); + mDragger.smoothSlideViewTo(toCapture, childLeft, toCapture.getTop()); + lp.isPeeking = true; + invalidate(); + + closeOtherDrawer(); + + cancelChildViewTouch(); + } + } + + @Override + public boolean onEdgeLock(int edgeFlags) { + if (ALLOW_EDGE_LOCK) { + final View drawer = findDrawerWithGravity(mAbsGravity); + if (drawer != null && !isDrawerOpen(drawer)) { + closeDrawer(drawer); + } + return true; + } + return false; + } + + @Override + public void onEdgeDragStarted(int edgeFlags, int pointerId) { + final View toCapture; + if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT) { + toCapture = findDrawerWithGravity(Gravity.LEFT); + } else { + toCapture = findDrawerWithGravity(Gravity.RIGHT); + } + + if (toCapture != null && getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { + mDragger.captureChildView(toCapture, pointerId); + } + } + + @Override + public int getViewHorizontalDragRange(View child) { + return child.getWidth(); + } + + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + return Math.max(-child.getWidth(), Math.min(left, 0)); + } else { + final int width = getWidth(); + return Math.max(width - child.getWidth(), Math.min(left, width)); + } + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + return child.getTop(); + } + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + + public int gravity = Gravity.NO_GRAVITY; + float onScreen; + boolean isPeeking; + boolean knownOpen; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + this.gravity = a.getInt(0, Gravity.NO_GRAVITY); + a.recycle(); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(int width, int height, int gravity) { + this(width, height); + this.gravity = gravity; + } + + public LayoutParams(LayoutParams source) { + super(source); + this.gravity = source.gravity; + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + } + + /*class AccessibilityDelegate extends AccessibilityDelegateCompat { + private final Rect mTmpRect = new Rect(); + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info); + super.onInitializeAccessibilityNodeInfo(host, superNode); + + info.setSource(host); + final ViewParent parent = host.getParentForAccessibility(); + if (parent instanceof View) { + info.setParent((View) parent); + } + copyNodeInfoNoChildren(info, superNode); + + superNode.recycle(); + + addChildrenForAccessibility(info, (ViewGroup) host); + } + + private void addChildrenForAccessibility(AccessibilityNodeInfoCompat info, ViewGroup v) { + final int childCount = v.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = v.getChildAt(i); + if (filter(child)) { + continue; + } + + // Adding children that are marked as not important for + // accessibility will break the hierarchy, so we need to check + // that value and re-parent views if necessary. + final int importance = child.getImportantForAccessibility(); + switch (importance) { + case View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS: + // Always skip NO_HIDE views and their descendants. + break; + case View.IMPORTANT_FOR_ACCESSIBILITY_NO: + // Re-parent children of NO view groups, skip NO views. + if (child instanceof ViewGroup) { + addChildrenForAccessibility(info, (ViewGroup) child); + } + break; + case View.IMPORTANT_FOR_ACCESSIBILITY_AUTO: + // Force AUTO views to YES and add them. + child.setImportantForAccessibility( + View.IMPORTANT_FOR_ACCESSIBILITY_YES); + case View.IMPORTANT_FOR_ACCESSIBILITY_YES: + info.addChild(child); + break; + } + } + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + if (!filter(child)) { + return super.onRequestSendAccessibilityEvent(host, child, event); + } + return false; + } + + public boolean filter(View child) { + final View openDrawer = findOpenDrawer(); + return openDrawer != null && openDrawer != child; + } + + /** + * This should really be in AccessibilityNodeInfoCompat, but there unfortunately + * seem to be a few elements that are not easily cloneable using the underlying API. + * Leave it private here as it's not general-purpose useful. + */ + /*private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, + AccessibilityNodeInfoCompat src) { + final Rect rect = mTmpRect; + + src.getBoundsInParent(rect); + dest.setBoundsInParent(rect); + + src.getBoundsInScreen(rect); + dest.setBoundsInScreen(rect); + + dest.setVisibleToUser(src.isVisibleToUser()); + dest.setPackageName(src.getPackageName()); + dest.setClassName(src.getClassName()); + dest.setContentDescription(src.getContentDescription()); + + dest.setEnabled(src.isEnabled()); + dest.setClickable(src.isClickable()); + dest.setFocusable(src.isFocusable()); + dest.setFocused(src.isFocused()); + dest.setAccessibilityFocused(src.isAccessibilityFocused()); + dest.setSelected(src.isSelected()); + dest.setLongClickable(src.isLongClickable()); + + dest.addAction(src.getActions()); + } + }*/ +} diff --git a/src/com/cyanogenmod/filemanager/ui/widgets/ViewDragHelper.java b/src/com/cyanogenmod/filemanager/ui/widgets/ViewDragHelper.java new file mode 100644 index 00000000..d9cd711d --- /dev/null +++ b/src/com/cyanogenmod/filemanager/ui/widgets/ViewDragHelper.java @@ -0,0 +1,1450 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * (modified from android.support.v4.widget) + */ + +package com.cyanogenmod.filemanager.ui.widgets; + +import android.content.Context; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +import java.util.Arrays; + +/** + * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number + * of useful operations and state tracking for allowing a user to drag and reposition + * views within their parent ViewGroup. + */ +public class ViewDragHelper { + @SuppressWarnings("unused") + private static final String TAG = "ViewDragHelper"; + + /** + * A null/invalid pointer ID. + */ + public static final int INVALID_POINTER = -1; + + /** + * A view is not currently being dragged or animating as a result of a fling/snap. + */ + public static final int STATE_IDLE = 0; + + /** + * A view is currently being dragged. The position is currently changing as a result + * of user input or simulated user input. + */ + public static final int STATE_DRAGGING = 1; + + /** + * A view is currently settling into place as a result of a fling or + * predefined non-interactive motion. + */ + public static final int STATE_SETTLING = 2; + + /** + * Edge flag indicating that the left edge should be affected. + */ + public static final int EDGE_LEFT = 1 << 0; + + /** + * Edge flag indicating that the right edge should be affected. + */ + public static final int EDGE_RIGHT = 1 << 1; + + /** + * Edge flag indicating that the top edge should be affected. + */ + public static final int EDGE_TOP = 1 << 2; + + /** + * Edge flag indicating that the bottom edge should be affected. + */ + public static final int EDGE_BOTTOM = 1 << 3; + + /** + * Edge flag set indicating all edges should be affected. + */ + public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM; + + /** + * Indicates that a check should occur along the horizontal axis + */ + public static final int DIRECTION_HORIZONTAL = 1 << 0; + + /** + * Indicates that a check should occur along the vertical axis + */ + public static final int DIRECTION_VERTICAL = 1 << 1; + + /** + * Indicates that a check should occur along all axes + */ + public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + + private static final int EDGE_SIZE = 20; // dp + + private static final int BASE_SETTLE_DURATION = 256; // ms + private static final int MAX_SETTLE_DURATION = 600; // ms + + // Current drag state; idle, dragging or settling + private int mDragState; + + // Distance to travel before a drag may begin + private int mTouchSlop; + + // Last known position/pointer tracking + private int mActivePointerId = INVALID_POINTER; + private float[] mInitialMotionX; + private float[] mInitialMotionY; + private float[] mLastMotionX; + private float[] mLastMotionY; + private int[] mInitialEdgesTouched; + private int[] mEdgeDragsInProgress; + private int[] mEdgeDragsLocked; + private int mPointersDown; + + private VelocityTracker mVelocityTracker; + private float mMaxVelocity; + private float mMinVelocity; + + private int mEdgeSize; + private int mTrackingEdges; + + private Scroller mScroller; + + private final Callback mCallback; + + private View mCapturedView; + private boolean mReleaseInProgress; + + private final ViewGroup mParentView; + + /** + * A Callback is used as a communication channel with the ViewDragHelper back to the + * parent view using it. on*methods are invoked on siginficant events and several + * accessor methods are expected to provide the ViewDragHelper with more information + * about the state of the parent view upon request. The callback also makes decisions + * governing the range and draggability of child views. + */ + public static abstract class Callback { + /** + * Called when the drag state changes. See the STATE_* constants + * for more information. + * + * @param state The new drag state + * + * @see #STATE_IDLE + * @see #STATE_DRAGGING + * @see #STATE_SETTLING + */ + public void onViewDragStateChanged(int state) {} + + /** + * Called when the captured view's position changes as the result of a drag or settle. + * + * @param changedView View whose position changed + * @param left New X coordinate of the left edge of the view + * @param top New Y coordinate of the top edge of the view + * @param dx Change in X position from the last call + * @param dy Change in Y position from the last call + */ + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {} + + /** + * Called when a child view is captured for dragging or settling. The ID of the pointer + * currently dragging the captured view is supplied. If activePointerId is + * identified as {@link #INVALID_POINTER} the capture is programmatic instead of + * pointer-initiated. + * + * @param capturedChild Child view that was captured + * @param activePointerId Pointer id tracking the child capture + */ + public void onViewCaptured(View capturedChild, int activePointerId) {} + + /** + * Called when the child view is no longer being actively dragged. + * The fling velocity is also supplied, if relevant. The velocity values may + * be clamped to system minimums or maximums. + * + *

Calling code may decide to fling or otherwise release the view to let it + * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)} + * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes + * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING} + * and the view capture will not fully end until it comes to a complete stop. + * If neither of these methods is invoked before onViewReleased returns, + * the view will stop in place and the ViewDragHelper will return to + * {@link #STATE_IDLE}.

+ * + * @param releasedChild The captured child view now being released + * @param xvel X velocity of the pointer as it left the screen in pixels per second. + * @param yvel Y velocity of the pointer as it left the screen in pixels per second. + */ + public void onViewReleased(View releasedChild, float xvel, float yvel) {} + + /** + * Called when one of the subscribed edges in the parent view has been touched + * by the user while no child view is currently captured. + * + * @param edgeFlags A combination of edge flags describing the edge(s) currently touched + * @param pointerId ID of the pointer touching the described edge(s) + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void onEdgeTouched(int edgeFlags, int pointerId) {} + + /** + * Called when the given edge may become locked. This can happen if an edge drag + * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)} + * was called. This method should return true to lock this edge or false to leave it + * unlocked. The default behavior is to leave edges unlocked. + * + * @param edgeFlags A combination of edge flags describing the edge(s) locked + * @return true to lock the edge, false to leave it unlocked + */ + public boolean onEdgeLock(int edgeFlags) { + return false; + } + + /** + * Called when the user has started a deliberate drag away from one + * of the subscribed edges in the parent view while no child view is currently captured. + * + * @param edgeFlags A combination of edge flags describing the edge(s) dragged + * @param pointerId ID of the pointer touching the described edge(s) + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void onEdgeDragStarted(int edgeFlags, int pointerId) {} + + /** + * Called to determine the Z-order of child views. + * + * @param index the ordered position to query for + * @return index of the view that should be ordered at position index + */ + public int getOrderedChildIndex(int index) { + return index; + } + + /** + * Return the magnitude of a draggable child view's horizontal range of motion in pixels. + * This method should return 0 for views that cannot move horizontally. + * + * @param child Child view to check + * @return range of horizontal motion in pixels + */ + public int getViewHorizontalDragRange(View child) { + return 0; + } + + /** + * Return the magnitude of a draggable child view's vertical range of motion in pixels. + * This method should return 0 for views that cannot move vertically. + * + * @param child Child view to check + * @return range of vertical motion in pixels + */ + public int getViewVerticalDragRange(View child) { + return 0; + } + + /** + * Called when the user's input indicates that they want to capture the given child view + * with the pointer indicated by pointerId. The callback should return true if the user + * is permitted to drag the given view with the indicated pointer. + * + *

ViewDragHelper may call this method multiple times for the same view even if + * the view is already captured; this indicates that a new pointer is trying to take + * control of the view.

+ * + *

If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)} + * will follow if the capture is successful.

+ * + * @param child Child the user is attempting to capture + * @param pointerId ID of the pointer attempting the capture + * @return true if capture should be allowed, false otherwise + */ + public abstract boolean tryCaptureView(View child, int pointerId); + + /** + * Restrict the motion of the dragged child view along the horizontal axis. + * The default implementation does not allow horizontal motion; the extending + * class must override this method and provide the desired clamping. + * + * + * @param child Child view being dragged + * @param left Attempted motion along the X axis + * @param dx Proposed change in position for left + * @return The new clamped position for left + */ + public int clampViewPositionHorizontal(View child, int left, int dx) { + return 0; + } + + /** + * Restrict the motion of the dragged child view along the vertical axis. + * The default implementation does not allow vertical motion; the extending + * class must override this method and provide the desired clamping. + * + * + * @param child Child view being dragged + * @param top Attempted motion along the Y axis + * @param dy Proposed change in position for top + * @return The new clamped position for top + */ + public int clampViewPositionVertical(View child, int top, int dy) { + return 0; + } + } + + /** + * Interpolator defining the animation curve for mScroller + */ + private static final Interpolator sInterpolator = new Interpolator() { + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private final Runnable mSetIdleRunnable = new Runnable() { + public void run() { + setDragState(STATE_IDLE); + } + }; + + /** + * Factory method to create a new ViewDragHelper. + * + * @param forParent Parent view to monitor + * @param cb Callback to provide information and receive events + * @return a new ViewDragHelper instance + */ + public static ViewDragHelper create(ViewGroup forParent, Callback cb) { + return new ViewDragHelper(forParent.getContext(), forParent, cb); + } + + /** + * Factory method to create a new ViewDragHelper. + * + * @param forParent Parent view to monitor + * @param sensitivity Multiplier for how sensitive the helper should be about detecting + * the start of a drag. Larger values are more sensitive. 1.0f is normal. + * @param cb Callback to provide information and receive events + * @return a new ViewDragHelper instance + */ + public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { + final ViewDragHelper helper = create(forParent, cb); + helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); + return helper; + } + + /** + * Apps should use ViewDragHelper.create() to get a new instance. + * This will allow VDH to use internal compatibility implementations for different + * platform versions. + * + * @param context Context to initialize config-dependent params from + * @param forParent Parent view to monitor + */ + private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { + if (forParent == null) { + throw new IllegalArgumentException("Parent view may not be null"); + } + if (cb == null) { + throw new IllegalArgumentException("Callback may not be null"); + } + + mParentView = forParent; + mCallback = cb; + + final ViewConfiguration vc = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + mEdgeSize = (int) (EDGE_SIZE * density + 0.5f); + + mTouchSlop = vc.getScaledTouchSlop(); + mMaxVelocity = vc.getScaledMaximumFlingVelocity(); + mMinVelocity = vc.getScaledMinimumFlingVelocity(); + mScroller = new Scroller(context, sInterpolator); + } + + /** + * Set the minimum velocity that will be detected as having a magnitude greater than zero + * in pixels per second. Callback methods accepting a velocity will be clamped appropriately. + * + * @param minVel Minimum velocity to detect + */ + public void setMinVelocity(float minVel) { + mMinVelocity = minVel; + } + + /** + * Return the currently configured minimum velocity. Any flings with a magnitude less + * than this value in pixels per second. Callback methods accepting a velocity will receive + * zero as a velocity value if the real detected velocity was below this threshold. + * + * @return the minimum velocity that will be detected + */ + public float getMinVelocity() { + return mMinVelocity; + } + + /** + * Retrieve the current drag state of this helper. This will return one of + * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. + * @return The current drag state + */ + public int getViewDragState() { + return mDragState; + } + + /** + * Enable edge tracking for the selected edges of the parent view. + * The callback's {@link Callback#onEdgeTouched(int, int)} and + * {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked + * for edges for which edge tracking has been enabled. + * + * @param edgeFlags Combination of edge flags describing the edges to watch + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void setEdgeTrackingEnabled(int edgeFlags) { + mTrackingEdges = edgeFlags; + } + + /** + * Return the size of an edge. This is the range in pixels along the edges of this view + * that will actively detect edge touches or drags if edge tracking is enabled. + * + * @return The size of an edge in pixels + * @see #setEdgeTrackingEnabled(int) + */ + public int getEdgeSize() { + return mEdgeSize; + } + + /** + * Capture a specific child view for dragging within the parent. The callback will be notified + * but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to + * capture this view. + * + * @param childView Child view to capture + * @param activePointerId ID of the pointer that is dragging the captured child view + */ + public void captureChildView(View childView, int activePointerId) { + if (childView.getParent() != mParentView) { + throw new IllegalArgumentException("captureChildView: parameter must be a descendant " + + "of the ViewDragHelper's tracked parent view (" + mParentView + ")"); + } + + mCapturedView = childView; + mActivePointerId = activePointerId; + mCallback.onViewCaptured(childView, activePointerId); + setDragState(STATE_DRAGGING); + } + + /** + * @return The currently captured view, or null if no view has been captured. + */ + public View getCapturedView() { + return mCapturedView; + } + + /** + * @return The ID of the pointer currently dragging the captured view, + * or {@link #INVALID_POINTER}. + */ + public int getActivePointerId() { + return mActivePointerId; + } + + /** + * @return The minimum distance in pixels that the user must travel to initiate a drag + */ + public int getTouchSlop() { + return mTouchSlop; + } + + /** + * The result of a call to this method is equivalent to + * {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event. + */ + public void cancel() { + mActivePointerId = INVALID_POINTER; + clearMotionHistory(); + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** + * {@link #cancel()}, but also abort all motion in progress and snap to the end of any + * animation. + */ + public void abort() { + cancel(); + if (mDragState == STATE_SETTLING) { + final int oldX = mScroller.getCurrX(); + final int oldY = mScroller.getCurrY(); + mScroller.abortAnimation(); + final int newX = mScroller.getCurrX(); + final int newY = mScroller.getCurrY(); + mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY); + } + setDragState(STATE_IDLE); + } + + /** + * Animate the view child to the given (left, top) position. + * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} + * on each subsequent frame to continue the motion until it returns false. If this method + * returns false there is no further work to do to complete the movement. + * + *

This operation does not count as a capture event, though {@link #getCapturedView()} + * will still report the sliding view while the slide is in progress.

+ * + * @param child Child view to capture and animate + * @param finalLeft Final left position of child + * @param finalTop Final top position of child + * @return true if animation should continue through {@link #continueSettling(boolean)} calls + */ + public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { + mCapturedView = child; + mActivePointerId = INVALID_POINTER; + + return forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0); + } + + /** + * Settle the captured view at the given (left, top) position. + * The appropriate velocity from prior motion will be taken into account. + * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} + * on each subsequent frame to continue the motion until it returns false. If this method + * returns false there is no further work to do to complete the movement. + * + * @param finalLeft Settled left edge position for the captured view + * @param finalTop Settled top edge position for the captured view + * @return true if animation should continue through {@link #continueSettling(boolean)} calls + */ + public boolean settleCapturedViewAt(int finalLeft, int finalTop) { + if (!mReleaseInProgress) { + throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + + "Callback#onViewReleased"); + } + + return forceSettleCapturedViewAt(finalLeft, finalTop, + (int) mVelocityTracker.getXVelocity(mActivePointerId), + (int) mVelocityTracker.getYVelocity(mActivePointerId)); + } + + /** + * Settle the captured view at the given (left, top) position. + * + * @param finalLeft Target left position for the captured view + * @param finalTop Target top position for the captured view + * @param xvel Horizontal velocity + * @param yvel Vertical velocity + * @return true if animation should continue through {@link #continueSettling(boolean)} calls + */ + private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { + final int startLeft = mCapturedView.getLeft(); + final int startTop = mCapturedView.getTop(); + final int dx = finalLeft - startLeft; + final int dy = finalTop - startTop; + + if (dx == 0 && dy == 0) { + // Nothing to do. Send callbacks, be done. + mScroller.abortAnimation(); + setDragState(STATE_IDLE); + return false; + } + + final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); + mScroller.startScroll(startLeft, startTop, dx, dy, duration); + + setDragState(STATE_SETTLING); + return true; + } + + private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) { + xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity); + yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity); + final int absDx = Math.abs(dx); + final int absDy = Math.abs(dy); + final int absXVel = Math.abs(xvel); + final int absYVel = Math.abs(yvel); + final int addedVel = absXVel + absYVel; + final int addedDistance = absDx + absDy; + + final float xweight = xvel != 0 ? (float) absXVel / addedVel : + (float) absDx / addedDistance; + final float yweight = yvel != 0 ? (float) absYVel / addedVel : + (float) absDy / addedDistance; + + int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child)); + int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child)); + + return (int) (xduration * xweight + yduration * yweight); + } + + private int computeAxisDuration(int delta, int velocity, int motionRange) { + if (delta == 0) { + return 0; + } + + final int width = mParentView.getWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width); + final float distance = halfWidth + halfWidth * + distanceInfluenceForSnapDuration(distanceRatio); + + int duration; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float range = (float) Math.abs(delta) / motionRange; + duration = (int) ((range + 1) * BASE_SETTLE_DURATION); + } + return Math.min(duration, MAX_SETTLE_DURATION); + } + + /** + * Clamp the magnitude of value for absMin and absMax. + * If the value is below the minimum, it will be clamped to zero. + * If the value is above the maximum, it will be clamped to the maximum. + * + * @param value Value to clamp + * @param absMin Absolute value of the minimum significant value to return + * @param absMax Absolute value of the maximum value to return + * @return The clamped value with the same sign as value + */ + private int clampMag(int value, int absMin, int absMax) { + final int absValue = Math.abs(value); + if (absValue < absMin) return 0; + if (absValue > absMax) return value > 0 ? absMax : -absMax; + return value; + } + + /** + * Clamp the magnitude of value for absMin and absMax. + * If the value is below the minimum, it will be clamped to zero. + * If the value is above the maximum, it will be clamped to the maximum. + * + * @param value Value to clamp + * @param absMin Absolute value of the minimum significant value to return + * @param absMax Absolute value of the maximum value to return + * @return The clamped value with the same sign as value + */ + private float clampMag(float value, float absMin, float absMax) { + final float absValue = Math.abs(value); + if (absValue < absMin) return 0; + if (absValue > absMax) return value > 0 ? absMax : -absMax; + return value; + } + + private float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Settle the captured view based on standard free-moving fling behavior. + * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame + * to continue the motion until it returns false. + * + * @param minLeft Minimum X position for the view's left edge + * @param minTop Minimum Y position for the view's top edge + * @param maxLeft Maximum X position for the view's left edge + * @param maxTop Maximum Y position for the view's top edge + */ + public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) { + if (!mReleaseInProgress) { + throw new IllegalStateException("Cannot flingCapturedView outside of a call to " + + "Callback#onViewReleased"); + } + + mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), + (int) mVelocityTracker.getXVelocity(mActivePointerId), + (int) mVelocityTracker.getYVelocity(mActivePointerId), + minLeft, maxLeft, minTop, maxTop); + + setDragState(STATE_SETTLING); + } + + /** + * Move the captured settling view by the appropriate amount for the current time. + * If continueSettling returns true, the caller should call it again + * on the next frame to continue. + * + * @param deferCallbacks true if state callbacks should be deferred via posted message. + * Set this to true if you are calling this method from + * {@link android.view.View#computeScroll()} or similar methods + * invoked as part of layout or drawing. + * @return true if settle is still in progress + */ + public boolean continueSettling(boolean deferCallbacks) { + if (mDragState == STATE_SETTLING) { + boolean keepGoing = mScroller.computeScrollOffset(); + final int x = mScroller.getCurrX(); + final int y = mScroller.getCurrY(); + final int dx = x - mCapturedView.getLeft(); + final int dy = y - mCapturedView.getTop(); + + if (dx != 0) { + mCapturedView.offsetLeftAndRight(dx); + } + if (dy != 0) { + mCapturedView.offsetTopAndBottom(dy); + } + + if (dx != 0 || dy != 0) { + mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy); + } + + if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) { + // Close enough. The interpolator/scroller might think we're still moving + // but the user sure doesn't. + mScroller.abortAnimation(); + keepGoing = mScroller.isFinished(); + } + + if (!keepGoing) { + if (deferCallbacks) { + mParentView.post(mSetIdleRunnable); + } else { + setDragState(STATE_IDLE); + } + } + } + + return mDragState == STATE_SETTLING; + } + + /** + * Like all callback events this must happen on the UI thread, but release + * involves some extra semantics. During a release (mReleaseInProgress) + * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)} + * or {@link #flingCapturedView(int, int, int, int)}. + */ + private void dispatchViewReleased(float xvel, float yvel) { + mReleaseInProgress = true; + mCallback.onViewReleased(mCapturedView, xvel, yvel); + mReleaseInProgress = false; + + if (mDragState == STATE_DRAGGING) { + // onViewReleased didn't call a method that would have changed this. Go idle. + setDragState(STATE_IDLE); + } + } + + private void clearMotionHistory() { + if (mInitialMotionX == null) { + return; + } + Arrays.fill(mInitialMotionX, 0); + Arrays.fill(mInitialMotionY, 0); + Arrays.fill(mLastMotionX, 0); + Arrays.fill(mLastMotionY, 0); + Arrays.fill(mInitialEdgesTouched, 0); + Arrays.fill(mEdgeDragsInProgress, 0); + Arrays.fill(mEdgeDragsLocked, 0); + mPointersDown = 0; + } + + private void clearMotionHistory(int pointerId) { + if (mInitialMotionX == null) { + return; + } + mInitialMotionX[pointerId] = 0; + mInitialMotionY[pointerId] = 0; + mLastMotionX[pointerId] = 0; + mLastMotionY[pointerId] = 0; + mInitialEdgesTouched[pointerId] = 0; + mEdgeDragsInProgress[pointerId] = 0; + mEdgeDragsLocked[pointerId] = 0; + mPointersDown &= ~(1 << pointerId); + } + + private void ensureMotionHistorySizeForId(int pointerId) { + if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) { + float[] imx = new float[pointerId + 1]; + float[] imy = new float[pointerId + 1]; + float[] lmx = new float[pointerId + 1]; + float[] lmy = new float[pointerId + 1]; + int[] iit = new int[pointerId + 1]; + int[] edip = new int[pointerId + 1]; + int[] edl = new int[pointerId + 1]; + + if (mInitialMotionX != null) { + System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length); + System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length); + System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length); + System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length); + System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length); + System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length); + System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length); + } + + mInitialMotionX = imx; + mInitialMotionY = imy; + mLastMotionX = lmx; + mLastMotionY = lmy; + mInitialEdgesTouched = iit; + mEdgeDragsInProgress = edip; + mEdgeDragsLocked = edl; + } + } + + private void saveInitialMotion(float x, float y, int pointerId) { + ensureMotionHistorySizeForId(pointerId); + mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x; + mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y; + mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y); + mPointersDown |= 1 << pointerId; + } + + private void saveLastMotion(MotionEvent ev) { + final int pointerCount = ev.getPointerCount(); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = ev.getPointerId(i); + final float x = ev.getX(i); + final float y = ev.getY(i); + mLastMotionX[pointerId] = x; + mLastMotionY[pointerId] = y; + } + } + + /** + * Check if the given pointer ID represents a pointer that is currently down (to the best + * of the ViewDragHelper's knowledge). + * + *

The state used to report this information is populated by the methods + * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or + * {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not + * been called for all relevant MotionEvents to track, the information reported + * by this method may be stale or incorrect.

+ * + * @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent + * @return true if the pointer with the given ID is still down + */ + public boolean isPointerDown(int pointerId) { + return (mPointersDown & 1 << pointerId) != 0; + } + + void setDragState(int state) { + if (mDragState != state) { + mDragState = state; + mCallback.onViewDragStateChanged(state); + if (state == STATE_IDLE) { + mCapturedView = null; + } + } + } + + /** + * Attempt to capture the view with the given pointer ID. The callback will be involved. + * This will put us into the "dragging" state. If we've already captured this view with + * this pointer this method will immediately return true without consulting the callback. + * + * @param toCapture View to capture + * @param pointerId Pointer to capture with + * @return true if capture was successful + */ + boolean tryCaptureViewForDrag(View toCapture, int pointerId) { + if (toCapture == mCapturedView && mActivePointerId == pointerId) { + // Already done! + return true; + } + if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { + mActivePointerId = pointerId; + captureChildView(toCapture, pointerId); + return true; + } + return false; + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels along the X axis + * @param dy Delta scrolled in pixels along the Y axis + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && + y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && + canScroll(child, true, dx, dy, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && (v.canScrollHorizontally(-dx) || + v.canScrollVertically(-dy)); + } + + /** + * Check if this event as provided to the parent view's onInterceptTouchEvent should + * cause the parent to intercept the touch event stream. + * + * @param ev MotionEvent provided to onInterceptTouchEvent + * @return true if the parent view should return true from onInterceptTouchEvent + */ + public boolean shouldInterceptTouchEvent(MotionEvent ev) { + final int action = ev.getActionMasked(); + final int actionIndex = ev.getActionIndex(); + + if (action == MotionEvent.ACTION_DOWN) { + // Reset things for a new event stream, just in case we didn't get + // the whole previous stream. + cancel(); + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + final int pointerId = ev.getPointerId(0); + saveInitialMotion(x, y, pointerId); + + final View toCapture = findTopChildUnder((int) x, (int) y); + + // Catch a settling view if possible. + if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { + tryCaptureViewForDrag(toCapture, pointerId); + } + + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + break; + } + + case MotionEvent.ACTION_POINTER_DOWN: { + final int pointerId = ev.getPointerId(actionIndex); + final float x = ev.getX(actionIndex); + final float y = ev.getY(actionIndex); + + saveInitialMotion(x, y, pointerId); + + // A ViewDragHelper can only manipulate one view at a time. + if (mDragState == STATE_IDLE) { + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + } else if (mDragState == STATE_SETTLING) { + // Catch a settling view if possible. + final View toCapture = findTopChildUnder((int) x, (int) y); + if (toCapture == mCapturedView) { + tryCaptureViewForDrag(toCapture, pointerId); + } + } + break; + } + + case MotionEvent.ACTION_MOVE: { + // First to cross a touch slop over a draggable view wins. Also report edge drags. + final int pointerCount = ev.getPointerCount(); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = ev.getPointerId(i); + final float x = ev.getX(i); + final float y = ev.getY(i); + final float dx = x - mInitialMotionX[pointerId]; + final float dy = y - mInitialMotionY[pointerId]; + + reportNewEdgeDrags(dx, dy, pointerId); + if (mDragState == STATE_DRAGGING) { + // Callback might have started an edge drag + break; + } + + final View toCapture = findTopChildUnder((int) x, (int) y); + if (toCapture != null && checkTouchSlop(toCapture, dx, dy) && + tryCaptureViewForDrag(toCapture, pointerId)) { + break; + } + } + saveLastMotion(ev); + break; + } + + case MotionEvent.ACTION_POINTER_UP: { + final int pointerId = ev.getPointerId(actionIndex); + clearMotionHistory(pointerId); + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + cancel(); + break; + } + } + + return mDragState == STATE_DRAGGING; + } + + /** + * Process a touch event received by the parent view. This method will dispatch callback events + * as needed before returning. The parent view's onTouchEvent implementation should call this. + * + * @param ev The touch event received by the parent view + */ + public void processTouchEvent(MotionEvent ev) { + final int action = ev.getActionMasked(); + final int actionIndex = ev.getActionIndex(); + + if (action == MotionEvent.ACTION_DOWN) { + // Reset things for a new event stream, just in case we didn't get + // the whole previous stream. + cancel(); + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + final int pointerId = ev.getPointerId(0); + final View toCapture = findTopChildUnder((int) x, (int) y); + + saveInitialMotion(x, y, pointerId); + + // Since the parent is already directly processing this touch event, + // there is no reason to delay for a slop before dragging. + // Start immediately if possible. + tryCaptureViewForDrag(toCapture, pointerId); + + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + break; + } + + case MotionEvent.ACTION_POINTER_DOWN: { + final int pointerId = ev.getPointerId(actionIndex); + final float x = ev.getX(actionIndex); + final float y = ev.getY(actionIndex); + + saveInitialMotion(x, y, pointerId); + + // A ViewDragHelper can only manipulate one view at a time. + if (mDragState == STATE_IDLE) { + // If we're idle we can do anything! Treat it like a normal down event. + + final View toCapture = findTopChildUnder((int) x, (int) y); + tryCaptureViewForDrag(toCapture, pointerId); + + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + } else if (isCapturedViewUnder((int) x, (int) y)) { + // We're still tracking a captured view. If the same view is under this + // point, we'll swap to controlling it with this pointer instead. + // (This will still work if we're "catching" a settling view.) + + tryCaptureViewForDrag(mCapturedView, pointerId); + } + break; + } + + case MotionEvent.ACTION_MOVE: { + if (mDragState == STATE_DRAGGING) { + final int index = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(index); + final float y = ev.getY(index); + final int idx = (int) (x - mLastMotionX[mActivePointerId]); + final int idy = (int) (y - mLastMotionY[mActivePointerId]); + + dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); + + saveLastMotion(ev); + } else { + // Check to see if any pointer is now over a draggable view. + final int pointerCount = ev.getPointerCount(); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = ev.getPointerId(i); + final float x = ev.getX(i); + final float y = ev.getY(i); + final float dx = x - mInitialMotionX[pointerId]; + final float dy = y - mInitialMotionY[pointerId]; + + reportNewEdgeDrags(dx, dy, pointerId); + if (mDragState == STATE_DRAGGING) { + // Callback might have started an edge drag. + break; + } + + final View toCapture = findTopChildUnder((int) x, (int) y); + if (checkTouchSlop(toCapture, dx, dy) && + tryCaptureViewForDrag(toCapture, pointerId)) { + break; + } + } + saveLastMotion(ev); + } + break; + } + + case MotionEvent.ACTION_POINTER_UP: { + final int pointerId = ev.getPointerId(actionIndex); + if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) { + // Try to find another pointer that's still holding on to the captured view. + int newActivePointer = INVALID_POINTER; + final int pointerCount = ev.getPointerCount(); + for (int i = 0; i < pointerCount; i++) { + final int id = ev.getPointerId(i); + if (id == mActivePointerId) { + // This one's going away, skip. + continue; + } + + final float x = ev.getX(i); + final float y = ev.getY(i); + if (findTopChildUnder((int) x, (int) y) == mCapturedView && + tryCaptureViewForDrag(mCapturedView, id)) { + newActivePointer = mActivePointerId; + break; + } + } + + if (newActivePointer == INVALID_POINTER) { + // We didn't find another pointer still touching the view, release it. + releaseViewForPointerUp(); + } + } + clearMotionHistory(pointerId); + break; + } + + case MotionEvent.ACTION_UP: { + if (mDragState == STATE_DRAGGING) { + releaseViewForPointerUp(); + } + cancel(); + break; + } + + case MotionEvent.ACTION_CANCEL: { + if (mDragState == STATE_DRAGGING) { + dispatchViewReleased(0, 0); + } + cancel(); + break; + } + } + } + + private void reportNewEdgeDrags(float dx, float dy, int pointerId) { + int dragsStarted = 0; + if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) { + dragsStarted |= EDGE_LEFT; + } + if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) { + dragsStarted |= EDGE_TOP; + } + if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) { + dragsStarted |= EDGE_RIGHT; + } + if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) { + dragsStarted |= EDGE_BOTTOM; + } + + if (dragsStarted != 0) { + mEdgeDragsInProgress[pointerId] |= dragsStarted; + mCallback.onEdgeDragStarted(dragsStarted, pointerId); + } + } + + private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) { + final float absDelta = Math.abs(delta); + final float absODelta = Math.abs(odelta); + + if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 || + (mEdgeDragsLocked[pointerId] & edge) == edge || + (mEdgeDragsInProgress[pointerId] & edge) == edge || + (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) { + return false; + } + if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) { + mEdgeDragsLocked[pointerId] |= edge; + return false; + } + return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop; + } + + /** + * Check if we've crossed a reasonable touch slop for the given child view. + * If the child cannot be dragged along the horizontal or vertical axis, motion + * along that axis will not count toward the slop check. + * + * @param child Child to check + * @param dx Motion since initial position along X axis + * @param dy Motion since initial position along Y axis + * @return true if the touch slop has been crossed + */ + private boolean checkTouchSlop(View child, float dx, float dy) { + if (child == null) { + return false; + } + final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0; + final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0; + + if (checkHorizontal && checkVertical) { + return dx * dx + dy * dy > mTouchSlop * mTouchSlop; + } else if (checkHorizontal) { + return Math.abs(dx) > mTouchSlop; + } else if (checkVertical) { + return Math.abs(dy) > mTouchSlop; + } + return false; + } + + /** + * Check if any pointer tracked in the current gesture has crossed + * the required slop threshold. + * + *

This depends on internal state populated by + * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or + * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on + * the results of this method after all currently available touch data + * has been provided to one of these two methods.

+ * + * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, + * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL} + * @return true if the slop threshold has been crossed, false otherwise + */ + public boolean checkTouchSlop(int directions) { + final int count = mInitialMotionX.length; + for (int i = 0; i < count; i++) { + if (checkTouchSlop(directions, i)) { + return true; + } + } + return false; + } + + /** + * Check if the specified pointer tracked in the current gesture has crossed + * the required slop threshold. + * + *

This depends on internal state populated by + * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or + * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on + * the results of this method after all currently available touch data + * has been provided to one of these two methods.

+ * + * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, + * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL} + * @param pointerId ID of the pointer to slop check as specified by MotionEvent + * @return true if the slop threshold has been crossed, false otherwise + */ + public boolean checkTouchSlop(int directions, int pointerId) { + if (!isPointerDown(pointerId)) { + return false; + } + + final boolean checkHorizontal = (directions & DIRECTION_HORIZONTAL) == DIRECTION_HORIZONTAL; + final boolean checkVertical = (directions & DIRECTION_VERTICAL) == DIRECTION_VERTICAL; + + final float dx = mLastMotionX[pointerId] - mInitialMotionX[pointerId]; + final float dy = mLastMotionY[pointerId] - mInitialMotionY[pointerId]; + + if (checkHorizontal && checkVertical) { + return dx * dx + dy * dy > mTouchSlop * mTouchSlop; + } else if (checkHorizontal) { + return Math.abs(dx) > mTouchSlop; + } else if (checkVertical) { + return Math.abs(dy) > mTouchSlop; + } + return false; + } + + /** + * Check if any of the edges specified were initially touched in the currently active gesture. + * If there is no currently active gesture this method will return false. + * + * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, + * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and + * {@link #EDGE_ALL} + * @return true if any of the edges specified were initially touched in the current gesture + */ + public boolean isEdgeTouched(int edges) { + final int count = mInitialEdgesTouched.length; + for (int i = 0; i < count; i++) { + if (isEdgeTouched(edges, i)) { + return true; + } + } + return false; + } + + /** + * Check if any of the edges specified were initially touched by the pointer with + * the specified ID. If there is no currently active gesture or if there is no pointer with + * the given ID currently down this method will return false. + * + * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, + * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and + * {@link #EDGE_ALL} + * @return true if any of the edges specified were initially touched in the current gesture + */ + public boolean isEdgeTouched(int edges, int pointerId) { + return isPointerDown(pointerId) && (mInitialEdgesTouched[pointerId] & edges) != 0; + } + + private void releaseViewForPointerUp() { + mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); + final float xvel = clampMag( + mVelocityTracker.getXVelocity(mActivePointerId), + mMinVelocity, mMaxVelocity); + final float yvel = clampMag( + mVelocityTracker.getYVelocity(mActivePointerId), + mMinVelocity, mMaxVelocity); + dispatchViewReleased(xvel, yvel); + } + + private void dragTo(int left, int top, int dx, int dy) { + int clampedX = left; + int clampedY = top; + final int oldLeft = mCapturedView.getLeft(); + final int oldTop = mCapturedView.getTop(); + if (dx != 0) { + clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); + mCapturedView.offsetLeftAndRight(clampedX - oldLeft); + } + if (dy != 0) { + clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); + mCapturedView.offsetTopAndBottom(clampedY - oldTop); + } + + if (dx != 0 || dy != 0) { + final int clampedDx = clampedX - oldLeft; + final int clampedDy = clampedY - oldTop; + mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, + clampedDx, clampedDy); + } + } + + /** + * Determine if the currently captured view is under the given point in the + * parent view's coordinate system. If there is no captured view this method + * will return false. + * + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return true if the captured view is under the given point, false otherwise + */ + public boolean isCapturedViewUnder(int x, int y) { + return isViewUnder(mCapturedView, x, y); + } + + /** + * Determine if the supplied view is under the given point in the + * parent view's coordinate system. + * + * @param view Child view of the parent to hit test + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return true if the supplied view is under the given point, false otherwise + */ + public boolean isViewUnder(View view, int x, int y) { + if (view == null) { + return false; + } + return x >= view.getLeft() && + x < view.getRight() && + y >= view.getTop() && + y < view.getBottom(); + } + + /** + * Find the topmost child under the given point within the parent view's coordinate system. + * The child order is determined using {@link Callback#getOrderedChildIndex(int)}. + * + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return The topmost child view under (x, y) or null if none found. + */ + public View findTopChildUnder(int x, int y) { + final int childCount = mParentView.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i)); + if (x >= child.getLeft() && x < child.getRight() && + y >= child.getTop() && y < child.getBottom()) { + return child; + } + } + return null; + } + + private int getEdgesTouched(int x, int y) { + int result = 0; + + if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT; + if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP; + if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT; + if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM; + + return result; + } +} diff --git a/themes/res/drawable-hdpi/ic_holo_dark_navigation_drawer.png b/themes/res/drawable-hdpi/ic_holo_dark_navigation_drawer.png new file mode 100644 index 00000000..48faf108 Binary files /dev/null and b/themes/res/drawable-hdpi/ic_holo_dark_navigation_drawer.png differ diff --git a/themes/res/drawable-mdpi/ic_holo_dark_navigation_drawer.png b/themes/res/drawable-mdpi/ic_holo_dark_navigation_drawer.png new file mode 100644 index 00000000..9680d157 Binary files /dev/null and b/themes/res/drawable-mdpi/ic_holo_dark_navigation_drawer.png differ diff --git a/themes/res/drawable-nodpi/dark_theme_preview.png b/themes/res/drawable-nodpi/dark_theme_preview.png index c18581f9..0ba30cd7 100644 Binary files a/themes/res/drawable-nodpi/dark_theme_preview.png and b/themes/res/drawable-nodpi/dark_theme_preview.png differ diff --git a/themes/res/drawable-xhdpi/ic_holo_dark_navigation_drawer.png b/themes/res/drawable-xhdpi/ic_holo_dark_navigation_drawer.png new file mode 100644 index 00000000..e2e12be6 Binary files /dev/null and b/themes/res/drawable-xhdpi/ic_holo_dark_navigation_drawer.png differ diff --git a/themes/res/drawable-xxhdpi/ic_holo_dark_navigation_drawer.png b/themes/res/drawable-xxhdpi/ic_holo_dark_navigation_drawer.png new file mode 100644 index 00000000..4c1220dc Binary files /dev/null and b/themes/res/drawable-xxhdpi/ic_holo_dark_navigation_drawer.png differ diff --git a/themes/res/values/dark_theme.xml b/themes/res/values/dark_theme.xml index 6bf10f49..c55371bb 100644 --- a/themes/res/values/dark_theme.xml +++ b/themes/res/values/dark_theme.xml @@ -72,6 +72,12 @@ #9933b5e5 + + #ff222222 + + + @drawable/ic_holo_dark_navigation_drawer + @drawable/ic_holo_dark_fs_locked -- cgit v1.2.3