François Gautrais 9 лет назад
Родитель
Сommit
fbbe35ed81
59 измененных файлов с 3488 добавлено и 73 удалено
  1. 4 1
      app/src/main/AndroidManifest.xml
  2. 23 23
      app/src/main/java/app/brest/testmin3d/ARActivity.java
  3. 109 0
      app/src/main/java/app/brest/testmin3d/AppCompatPreferenceActivity.java
  4. 74 11
      app/src/main/java/app/brest/testmin3d/MenuActivity.java
  5. 2 5
      app/src/main/java/app/brest/testmin3d/ResourceListActivity.java
  6. 254 0
      app/src/main/java/app/brest/testmin3d/SettingsActivity.java
  7. 9 1
      app/src/main/java/app/brest/testmin3d/SplashActivity.java
  8. 15 2
      app/src/main/java/app/brest/testmin3d/ViewerActivity.java
  9. 4 3
      app/src/main/java/app/brest/utils/SensorsManager.java
  10. 7 3
      app/src/main/java/app/brest/utils/app/brest/game/Game.java
  11. 4 2
      app/src/main/java/app/brest/utils/app/brest/game/Player.java
  12. 60 0
      app/src/main/java/uk/co/senab/photoview/Compat.java
  13. 100 0
      app/src/main/java/uk/co/senab/photoview/DefaultOnDoubleTapListener.java
  14. 269 0
      app/src/main/java/uk/co/senab/photoview/IPhotoView.java
  15. 273 0
      app/src/main/java/uk/co/senab/photoview/PhotoView.java
  16. 1192 0
      app/src/main/java/uk/co/senab/photoview/PhotoViewAttacher.java
  17. 149 0
      app/src/main/java/uk/co/senab/photoview/gestures/CupcakeGestureDetector.java
  18. 91 0
      app/src/main/java/uk/co/senab/photoview/gestures/EclairGestureDetector.java
  19. 72 0
      app/src/main/java/uk/co/senab/photoview/gestures/FroyoGestureDetector.java
  20. 30 0
      app/src/main/java/uk/co/senab/photoview/gestures/GestureDetector.java
  21. 27 0
      app/src/main/java/uk/co/senab/photoview/gestures/OnGestureListener.java
  22. 42 0
      app/src/main/java/uk/co/senab/photoview/gestures/VersionedGestureDetector.java
  23. 35 0
      app/src/main/java/uk/co/senab/photoview/log/LogManager.java
  24. 42 0
      app/src/main/java/uk/co/senab/photoview/log/Logger.java
  25. 76 0
      app/src/main/java/uk/co/senab/photoview/log/LoggerDefault.java
  26. 61 0
      app/src/main/java/uk/co/senab/photoview/scrollerproxy/GingerScroller.java
  27. 33 0
      app/src/main/java/uk/co/senab/photoview/scrollerproxy/IcsScroller.java
  28. 58 0
      app/src/main/java/uk/co/senab/photoview/scrollerproxy/PreGingerScroller.java
  29. 48 0
      app/src/main/java/uk/co/senab/photoview/scrollerproxy/ScrollerProxy.java
  30. BIN
      app/src/main/res/drawable-hdpi/ic_info_black_24dp.png
  31. BIN
      app/src/main/res/drawable-hdpi/ic_notifications_black_24dp.png
  32. BIN
      app/src/main/res/drawable-hdpi/ic_sync_black_24dp.png
  33. BIN
      app/src/main/res/drawable-mdpi/ic_info_black_24dp.png
  34. BIN
      app/src/main/res/drawable-mdpi/ic_notifications_black_24dp.png
  35. BIN
      app/src/main/res/drawable-mdpi/ic_sync_black_24dp.png
  36. 9 0
      app/src/main/res/drawable-v21/ic_info_black_24dp.xml
  37. 9 0
      app/src/main/res/drawable-v21/ic_notifications_black_24dp.xml
  38. 9 0
      app/src/main/res/drawable-v21/ic_sync_black_24dp.xml
  39. BIN
      app/src/main/res/drawable-xhdpi/ic_info_black_24dp.png
  40. BIN
      app/src/main/res/drawable-xhdpi/ic_notifications_black_24dp.png
  41. BIN
      app/src/main/res/drawable-xhdpi/ic_sync_black_24dp.png
  42. BIN
      app/src/main/res/drawable-xxhdpi/ic_info_black_24dp.png
  43. BIN
      app/src/main/res/drawable-xxhdpi/ic_notifications_black_24dp.png
  44. BIN
      app/src/main/res/drawable-xxhdpi/ic_sync_black_24dp.png
  45. BIN
      app/src/main/res/drawable-xxxhdpi/ic_info_black_24dp.png
  46. BIN
      app/src/main/res/drawable-xxxhdpi/ic_notifications_black_24dp.png
  47. BIN
      app/src/main/res/drawable-xxxhdpi/ic_sync_black_24dp.png
  48. BIN
      app/src/main/res/drawable/carte.png
  49. BIN
      app/src/main/res/drawable/game_medium_map.png
  50. BIN
      app/src/main/res/drawable/inventaire.png
  51. BIN
      app/src/main/res/drawable/options.png
  52. BIN
      app/src/main/res/drawable/scanner.png
  53. 137 21
      app/src/main/res/layout/activity_menu.xml
  54. 1 1
      app/src/main/res/layout/activity_viewer.xml
  55. 59 0
      app/src/main/res/values/strings.xml
  56. 21 0
      app/src/main/res/xml/pref_data_sync.xml
  57. 33 0
      app/src/main/res/xml/pref_general.xml
  58. 20 0
      app/src/main/res/xml/pref_headers.xml
  59. 27 0
      app/src/main/res/xml/pref_notification.xml

+ 4 - 1
app/src/main/AndroidManifest.xml

@@ -43,7 +43,10 @@
         <activity
             android:name=".MenuActivity"
             android:label="@string/title_activity_menu"
-            android:theme="@style/AppTheme.NoActionBar"></activity>
+            android:theme="@style/AppTheme.NoActionBar" />
+        <activity
+            android:name=".SettingsActivity"
+            android:label="@string/title_activity_settings"></activity>
     </application>
 
 </manifest>

+ 23 - 23
app/src/main/java/app/brest/testmin3d/ARActivity.java

@@ -25,6 +25,7 @@ import app.brest.utils.ResourceManager;
 import app.brest.utils.SensorsManager;
 import app.brest.utils.app.brest.game.Game;
 import app.brest.utils.app.brest.game.Resource;
+import app.brest.utils.geometry.GPSPoint;
 import min3d.core.Object3d;
 import min3d.core.Object3dContainer;
 import min3d.core.RendererActivity;
@@ -47,7 +48,8 @@ public class ARActivity extends RendererActivity
     private TextView mGps;
     private final Object mLock = new Object();
     protected boolean isPausing=false;
-
+    protected static GPSPoint mLocation;
+    protected static boolean mLocationUpdated=false;
     private Game mGame;
 
 
@@ -172,22 +174,8 @@ public class ARActivity extends RendererActivity
     {
         super.onCreate(savedInstanceState);
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-
-        Game g = Game.load(this);
-
-        if(getIntent().hasExtra("game_name"))
-        {
-            String n = getIntent().getStringExtra("game_name");
-            mGame = new Game(n, this);
-
-        }else if (g!= null) {
-            mGame = g;
-            mGame.newSensorManager(this);
-        }else
-        {
-            throw new Error("Le jeu n'est pas sauvegardé");
-        }
-        ResourceListActivity.setGame(mGame);
+        mGame = (Game) getIntent().getSerializableExtra("game");
+        mGame.newSensorManager(this);
 
     }
 
@@ -244,6 +232,8 @@ public class ARActivity extends RendererActivity
             }
         });
 
+        setGpsText();
+
     }
 
 
@@ -271,14 +261,18 @@ public class ARActivity extends RendererActivity
 
     }
 
-    public void setGpsText(final double lat, final double lon)
+    public void setGpsText()
     {
-        mGps.getHandler().post(new Runnable() {
+        if(mLocationUpdated) {
+            final GPSPoint gps = mLocation;
+            mGps.getHandler().post(new Runnable() {
 
-            public void run() {
-                mGps.setText("Latitude: "+lat+"°\n Longitude: "+lon+"\n");
-            }
-        });
+                public void run() {
+                    mGps.setText("Latitude: " + gps.getX() + "°\n Longitude: " + gps.getY() + "\n");
+                }
+            });
+            mLocationUpdated=false;
+        }
     }
 
     public void onSaveInstanceState(Bundle savedInstanceState) {
@@ -314,4 +308,10 @@ public class ARActivity extends RendererActivity
 
     }
 
+    public static void setLocation(GPSPoint p)
+    {
+        mLocation = p;
+        mLocationUpdated=true;
+    }
+
 }

+ 109 - 0
app/src/main/java/app/brest/testmin3d/AppCompatPreferenceActivity.java

@@ -0,0 +1,109 @@
+package app.brest.testmin3d;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.Nullable;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatDelegate;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
+ * to be used with AppCompat.
+ */
+public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
+
+    private AppCompatDelegate mDelegate;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        getDelegate().installViewFactory();
+        getDelegate().onCreate(savedInstanceState);
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    protected void onPostCreate(Bundle savedInstanceState) {
+        super.onPostCreate(savedInstanceState);
+        getDelegate().onPostCreate(savedInstanceState);
+    }
+
+    public ActionBar getSupportActionBar() {
+        return getDelegate().getSupportActionBar();
+    }
+
+    public void setSupportActionBar(@Nullable Toolbar toolbar) {
+        getDelegate().setSupportActionBar(toolbar);
+    }
+
+    @Override
+    public MenuInflater getMenuInflater() {
+        return getDelegate().getMenuInflater();
+    }
+
+    @Override
+    public void setContentView(@LayoutRes int layoutResID) {
+        getDelegate().setContentView(layoutResID);
+    }
+
+    @Override
+    public void setContentView(View view) {
+        getDelegate().setContentView(view);
+    }
+
+    @Override
+    public void setContentView(View view, ViewGroup.LayoutParams params) {
+        getDelegate().setContentView(view, params);
+    }
+
+    @Override
+    public void addContentView(View view, ViewGroup.LayoutParams params) {
+        getDelegate().addContentView(view, params);
+    }
+
+    @Override
+    protected void onPostResume() {
+        super.onPostResume();
+        getDelegate().onPostResume();
+    }
+
+    @Override
+    protected void onTitleChanged(CharSequence title, int color) {
+        super.onTitleChanged(title, color);
+        getDelegate().setTitle(title);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        getDelegate().onConfigurationChanged(newConfig);
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        getDelegate().onStop();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        getDelegate().onDestroy();
+    }
+
+    public void invalidateOptionsMenu() {
+        getDelegate().invalidateOptionsMenu();
+    }
+
+    private AppCompatDelegate getDelegate() {
+        if (mDelegate == null) {
+            mDelegate = AppCompatDelegate.create(this, null);
+        }
+        return mDelegate;
+    }
+}

+ 74 - 11
app/src/main/java/app/brest/testmin3d/MenuActivity.java

@@ -1,31 +1,94 @@
 package app.brest.testmin3d;
 
+import android.content.Intent;
+import android.graphics.Point;
 import android.os.Bundle;
 import android.support.design.widget.FloatingActionButton;
 import android.support.design.widget.Snackbar;
 import android.support.v7.app.AppCompatActivity;
 import android.support.v7.widget.Toolbar;
+import android.view.Display;
 import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
 
 import app.brest.testmin3d.R;
+import app.brest.utils.app.brest.game.Game;
 
 public class MenuActivity extends AppCompatActivity {
+    protected Game mGame;
+    protected ImageButton mICarte;
+    protected ImageButton mIInventaire;
+    protected ImageButton mIOptions;
+    protected ImageButton mIScanner;
+
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_menu);
-        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
-        setSupportActionBar(toolbar);
-
-        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
-        fab.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
-                        .setAction("Action", null).show();
-            }
-        });
+
+        Display display = getWindowManager().getDefaultDisplay();
+        Point size = new Point();
+        display.getSize(size);
+        int width = size.x;
+        int height = size.y;
+        int buttonHieght = (height-4*40)/5;
+        mICarte = (ImageButton) findViewById(R.id.ib_carte);
+        mIInventaire = (ImageButton) findViewById(R.id.ib_inventaire);
+        mIOptions = (ImageButton) findViewById(R.id.ib_options);
+        mIScanner = (ImageButton) findViewById(R.id.ib_scanner);
+
+        mICarte.setLayoutParams(new LinearLayout.LayoutParams(buttonHieght, buttonHieght));
+        mIInventaire.setLayoutParams(new LinearLayout.LayoutParams(buttonHieght, buttonHieght));
+        mIOptions.setLayoutParams(new LinearLayout.LayoutParams(buttonHieght, buttonHieght));
+        mIScanner.setLayoutParams(new LinearLayout.LayoutParams(buttonHieght, buttonHieght));
+
+        Game g = Game.load(this);
+
+        if(getIntent().hasExtra("game_name"))
+        {
+            String str = getIntent().getStringExtra("game_name");
+            mGame = new Game(str, this);
+
+        }else if (g!= null) {
+            mGame = g;
+            mGame.newSensorManager(this);
+        }else
+        {
+            throw new Error("Le jeu n'est pas sauvegardé");
+        }
+    }
+
+
+    public void onOptionsClick(View v)
+    {
+        Intent intent = new Intent(this, SettingsActivity.class);
+        startActivity(intent);
+    }
+
+
+    public void onCarteClick(View v)
+    {
+        Intent intent = new Intent(this, ViewerActivity.class);
+        intent.putExtra("map", mGame.getName()+"_map");
+        startActivity(intent);
+    }
+
+
+    public void onScannerClick(View v)
+    {
+        Intent intent = new Intent(this, ARActivity.class);
+        intent.putExtra("game", mGame);
+        startActivity(intent);
+    }
+
+
+    public void onInventaireClick(View v)
+    {
+        Intent intent = new Intent(this, ResourceListActivity.class);
+        intent.putExtra("game", mGame);
+        startActivity(intent);
     }
 
 }

+ 2 - 5
app/src/main/java/app/brest/testmin3d/ResourceListActivity.java

@@ -31,7 +31,7 @@ import app.brest.utils.app.brest.game.Resource;
  */
 public class ResourceListActivity extends AppCompatActivity {
 
-    protected static Game mGame;
+    protected Game mGame;
     protected LinearLayout mLayout;
 
 
@@ -73,6 +73,7 @@ public class ResourceListActivity extends AppCompatActivity {
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_resource_list);
+        mGame = (Game)getIntent().getSerializableExtra("game");
         if(mGame!=null)
         {
             mGame.pickResource("camaro");
@@ -82,9 +83,5 @@ public class ResourceListActivity extends AppCompatActivity {
         fill();
     }
 
-    public static void setGame(Game g)
-    {
-        mGame = g;
-    }
 
 }

+ 254 - 0
app/src/main/java/app/brest/testmin3d/SettingsActivity.java

@@ -0,0 +1,254 @@
+package app.brest.testmin3d;
+
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.support.v7.app.ActionBar;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.preference.RingtonePreference;
+import android.text.TextUtils;
+import android.view.MenuItem;
+
+import java.util.List;
+
+/**
+ * A {@link PreferenceActivity} that presents a set of application settings. On
+ * handset devices, settings are presented as a single list. On tablets,
+ * settings are split by category, with category headers shown to the left of
+ * the list of settings.
+ * <p/>
+ * See <a href="http://developer.android.com/design/patterns/settings.html">
+ * Android Design: Settings</a> for design guidelines and the <a
+ * href="http://developer.android.com/guide/topics/ui/settings.html">Settings
+ * API Guide</a> for more information on developing a Settings UI.
+ */
+public class SettingsActivity extends AppCompatPreferenceActivity {
+    /**
+     * A preference value change listener that updates the preference's summary
+     * to reflect its new value.
+     */
+    private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
+        @Override
+        public boolean onPreferenceChange(Preference preference, Object value) {
+            String stringValue = value.toString();
+
+            if (preference instanceof ListPreference) {
+                // For list preferences, look up the correct display value in
+                // the preference's 'entries' list.
+                ListPreference listPreference = (ListPreference) preference;
+                int index = listPreference.findIndexOfValue(stringValue);
+
+                // Set the summary to reflect the new value.
+                preference.setSummary(
+                        index >= 0
+                                ? listPreference.getEntries()[index]
+                                : null);
+
+            } else if (preference instanceof RingtonePreference) {
+                // For ringtone preferences, look up the correct display value
+                // using RingtoneManager.
+                if (TextUtils.isEmpty(stringValue)) {
+                    // Empty values correspond to 'silent' (no ringtone).
+                    preference.setSummary(R.string.pref_ringtone_silent);
+
+                } else {
+                    Ringtone ringtone = RingtoneManager.getRingtone(
+                            preference.getContext(), Uri.parse(stringValue));
+
+                    if (ringtone == null) {
+                        // Clear the summary if there was a lookup error.
+                        preference.setSummary(null);
+                    } else {
+                        // Set the summary to reflect the new ringtone display
+                        // name.
+                        String name = ringtone.getTitle(preference.getContext());
+                        preference.setSummary(name);
+                    }
+                }
+
+            } else {
+                // For all other preferences, set the summary to the value's
+                // simple string representation.
+                preference.setSummary(stringValue);
+            }
+            return true;
+        }
+    };
+
+    /**
+     * Helper method to determine if the device has an extra-large screen. For
+     * example, 10" tablets are extra-large.
+     */
+    private static boolean isXLargeTablet(Context context) {
+        return (context.getResources().getConfiguration().screenLayout
+                & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
+    }
+
+    /**
+     * Binds a preference's summary to its value. More specifically, when the
+     * preference's value is changed, its summary (line of text below the
+     * preference title) is updated to reflect the value. The summary is also
+     * immediately updated upon calling this method. The exact display format is
+     * dependent on the type of preference.
+     *
+     * @see #sBindPreferenceSummaryToValueListener
+     */
+    private static void bindPreferenceSummaryToValue(Preference preference) {
+        // Set the listener to watch for value changes.
+        preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
+
+        // Trigger the listener immediately with the preference's
+        // current value.
+        sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
+                PreferenceManager
+                        .getDefaultSharedPreferences(preference.getContext())
+                        .getString(preference.getKey(), ""));
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setupActionBar();
+    }
+
+    /**
+     * Set up the {@link android.app.ActionBar}, if the API is available.
+     */
+    private void setupActionBar() {
+        ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            // Show the Up button in the action bar.
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean onIsMultiPane() {
+        return isXLargeTablet(this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public void onBuildHeaders(List<Header> target) {
+        loadHeadersFromResource(R.xml.pref_headers, target);
+    }
+
+    /**
+     * This method stops fragment injection in malicious applications.
+     * Make sure to deny any unknown fragments here.
+     */
+    protected boolean isValidFragment(String fragmentName) {
+        return PreferenceFragment.class.getName().equals(fragmentName)
+                || GeneralPreferenceFragment.class.getName().equals(fragmentName)
+                || DataSyncPreferenceFragment.class.getName().equals(fragmentName)
+                || NotificationPreferenceFragment.class.getName().equals(fragmentName);
+    }
+
+    /**
+     * This fragment shows general preferences only. It is used when the
+     * activity is showing a two-pane settings UI.
+     */
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public static class GeneralPreferenceFragment extends PreferenceFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            addPreferencesFromResource(R.xml.pref_general);
+            setHasOptionsMenu(true);
+
+            // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+            // to their values. When their values change, their summaries are
+            // updated to reflect the new value, per the Android Design
+            // guidelines.
+            bindPreferenceSummaryToValue(findPreference("example_text"));
+            bindPreferenceSummaryToValue(findPreference("example_list"));
+        }
+
+        @Override
+        public boolean onOptionsItemSelected(MenuItem item) {
+            int id = item.getItemId();
+            if (id == android.R.id.home) {
+                startActivity(new Intent(getActivity(), SettingsActivity.class));
+                return true;
+            }
+            return super.onOptionsItemSelected(item);
+        }
+    }
+
+    /**
+     * This fragment shows notification preferences only. It is used when the
+     * activity is showing a two-pane settings UI.
+     */
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public static class NotificationPreferenceFragment extends PreferenceFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            addPreferencesFromResource(R.xml.pref_notification);
+            setHasOptionsMenu(true);
+
+            // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+            // to their values. When their values change, their summaries are
+            // updated to reflect the new value, per the Android Design
+            // guidelines.
+            bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone"));
+        }
+
+        @Override
+        public boolean onOptionsItemSelected(MenuItem item) {
+            int id = item.getItemId();
+            if (id == android.R.id.home) {
+                startActivity(new Intent(getActivity(), SettingsActivity.class));
+                return true;
+            }
+            return super.onOptionsItemSelected(item);
+        }
+    }
+
+    /**
+     * This fragment shows data and sync preferences only. It is used when the
+     * activity is showing a two-pane settings UI.
+     */
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public static class DataSyncPreferenceFragment extends PreferenceFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            addPreferencesFromResource(R.xml.pref_data_sync);
+            setHasOptionsMenu(true);
+
+            // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+            // to their values. When their values change, their summaries are
+            // updated to reflect the new value, per the Android Design
+            // guidelines.
+            bindPreferenceSummaryToValue(findPreference("sync_frequency"));
+        }
+
+        @Override
+        public boolean onOptionsItemSelected(MenuItem item) {
+            int id = item.getItemId();
+            if (id == android.R.id.home) {
+                startActivity(new Intent(getActivity(), SettingsActivity.class));
+                return true;
+            }
+            return super.onOptionsItemSelected(item);
+        }
+    }
+}

+ 9 - 1
app/src/main/java/app/brest/testmin3d/SplashActivity.java

@@ -20,7 +20,7 @@ public class SplashActivity extends AppCompatActivity {
         GPSPoint a = new GPSPoint(48.3845125, -4.493831);
         GPSPoint b = new GPSPoint(48.3840495, -4.5037464);
         Log.e("GPS", "OUT:"+a.getDistanceWith(b));
-        onClickNew(null);
+        onClickMenu(null);
     }
 
     public void onClickNew(View v)
@@ -30,6 +30,14 @@ public class SplashActivity extends AppCompatActivity {
         startActivity(intent);
     }
 
+
+    public void onClickMenu(View v)
+    {
+        Intent intent = new Intent(this, MenuActivity.class);
+        intent.putExtra("game_name", "game_medium");
+        startActivity(intent);
+    }
+
     public void onClickContinue(View v)
     {
         Intent intent = new Intent(this, ARActivity.class);

+ 15 - 2
app/src/main/java/app/brest/testmin3d/ViewerActivity.java

@@ -13,6 +13,7 @@ import android.view.WindowManager;
 import android.widget.Button;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
+import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import java.io.InputStream;
@@ -25,6 +26,7 @@ import min3d.core.RendererActivity;
 import min3d.parser.IParser;
 import min3d.parser.Parser;
 import min3d.vos.Light;
+import uk.co.senab.photoview.PhotoViewAttacher;
 
 /**
  * An example full-screen activity that shows and hides the system UI (i.e.
@@ -38,6 +40,7 @@ public class ViewerActivity extends RendererActivity {
     private ImageView mImageView;
     private Button mButton;
     private TextView mText;
+    private PhotoViewAttacher mAttacher;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -55,22 +58,32 @@ public class ViewerActivity extends RendererActivity {
             setTitle(mResource.getTitle());
         }
 
-        if(mResource != null && !getIntent().hasExtra("information"))
+        if(mResource != null && !getIntent().hasExtra("information") && !getIntent().hasExtra("map"))
         {
             if(mResource.getType().compareTo("Image")==0)
             {
                 int id = getResources().getIdentifier(mResource.getName()+"_res","drawable", getPackageName());
                 mImageView.setImageResource(id);
+                mAttacher = new PhotoViewAttacher(mImageView);
             }
             else if(mResource.getType().compareTo("3D")==0)
             {
                 mFrameLayout.addView(_glSurfaceView);
             }
-        }else if(mResource != null)
+        } else if(getIntent().hasExtra("map") )
+        {
+            int id = getResources().getIdentifier(getIntent().getStringExtra("map"),"drawable", getPackageName());
+            mImageView.setImageResource(id);
+            mAttacher = new PhotoViewAttacher(mImageView);
+            mButton.setVisibility(View.INVISIBLE);
+            mFrameLayout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT));
+        }
+        else if(mResource != null && getIntent().hasExtra("information"))
         {
             mButton.setVisibility(View.INVISIBLE);
             mText.setVisibility(View.VISIBLE);
             mText.setText(mResource.getComment());
+            mFrameLayout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT));
         }
 
     }

+ 4 - 3
app/src/main/java/app/brest/utils/SensorsManager.java

@@ -1,6 +1,7 @@
 package app.brest.utils;
 
 import android.Manifest;
+import android.app.Activity;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.hardware.Sensor;
@@ -43,7 +44,7 @@ public class SensorsManager implements SensorEventListener, LocationListener {
     private double mLongitude;
     private float mAccuracy;
     private boolean mIsLocalised = false;
-    private ARActivity mParent;
+    private Activity mParent;
 
     public float getAzimuth() {
         return mBufferX.average();
@@ -72,7 +73,7 @@ public class SensorsManager implements SensorEventListener, LocationListener {
         return new GPSPoint(mLongitude, mLatitude, mAccuracy);
     }
 
-    public SensorsManager(ARActivity parent ){
+    public SensorsManager(Activity parent ){
         mParent = parent;
         mLocationManager = (LocationManager) parent.getSystemService(parent.LOCATION_SERVICE);
 
@@ -161,7 +162,7 @@ public class SensorsManager implements SensorEventListener, LocationListener {
         mLatitude=location.getLatitude();
         mAccuracy=location.getAccuracy();
 
-        mParent.setGpsText(mLatitude, mLongitude);
+        ARActivity.setLocation(new GPSPoint(mLatitude, mLongitude, mAccuracy));
         //Toast.makeText(mParent,  "Longitude: "+location.getLongitude()+ " Latitude: "+location.getLatitude(), Toast.LENGTH_LONG).show();
     }
 

+ 7 - 3
app/src/main/java/app/brest/utils/app/brest/game/Game.java

@@ -33,11 +33,12 @@ public class Game  implements Serializable {
     protected int             mCurrentStage=0;
     protected Player          mPlayer;
     protected ResourceManager mResources;
+    protected String          mName;
 
-    public Game(String name, ARActivity act)
+    public Game(String name, Activity act)
     {
         JSONObject root = JSONLoader.load(act, name);
-
+        mName=name;
         mPlayer = new Player(act);
         try {
             JSONArray areas = root.getJSONArray("areas");
@@ -56,6 +57,9 @@ public class Game  implements Serializable {
     public int getNStages() {
         return mNStages;
     }
+    public String getName() {
+        return mName;
+    }
 
     public int getCurrentStage() {
         return mCurrentStage;
@@ -298,7 +302,7 @@ public class Game  implements Serializable {
         mResources.deleteAll3DModel();
     }
 
-    public void newSensorManager(ARActivity act)
+    public void newSensorManager(Activity act)
     {
         mPlayer.newSensorManager(act);
     }

+ 4 - 2
app/src/main/java/app/brest/utils/app/brest/game/Player.java

@@ -1,5 +1,7 @@
 package app.brest.utils.app.brest.game;
 
+import android.app.Activity;
+
 import java.io.Serializable;
 
 import app.brest.testmin3d.ARActivity;
@@ -12,7 +14,7 @@ import app.brest.utils.geometry.GPSPoint;
 public class Player implements Serializable{
     protected transient SensorsManager mSensors;
 
-    public Player(ARActivity sm)
+    public Player(Activity sm)
     {
         mSensors=new SensorsManager(sm);
     }
@@ -64,7 +66,7 @@ public class Player implements Serializable{
         return mSensors.getAngleZ();
     }
 
-    public void newSensorManager(ARActivity act)
+    public void newSensorManager(Activity act)
     {
         mSensors = new SensorsManager(act);
     }

+ 60 - 0
app/src/main/java/uk/co/senab/photoview/Compat.java

@@ -0,0 +1,60 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.view.MotionEvent;
+import android.view.View;
+
+public class Compat {
+
+    private static final int SIXTY_FPS_INTERVAL = 1000 / 60;
+
+    public static void postOnAnimation(View view, Runnable runnable) {
+        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
+            postOnAnimationJellyBean(view, runnable);
+        } else {
+            view.postDelayed(runnable, SIXTY_FPS_INTERVAL);
+        }
+    }
+
+    @TargetApi(16)
+    private static void postOnAnimationJellyBean(View view, Runnable runnable) {
+        view.postOnAnimation(runnable);
+    }
+
+    public static int getPointerIndex(int action) {
+        if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB)
+            return getPointerIndexHoneyComb(action);
+        else
+            return getPointerIndexEclair(action);
+    }
+
+    @SuppressWarnings("deprecation")
+    @TargetApi(Build.VERSION_CODES.ECLAIR)
+    private static int getPointerIndexEclair(int action) {
+        return (action & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;
+    }
+
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    private static int getPointerIndexHoneyComb(int action) {
+        return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+    }
+
+}

+ 100 - 0
app/src/main/java/uk/co/senab/photoview/DefaultOnDoubleTapListener.java

@@ -0,0 +1,100 @@
+package uk.co.senab.photoview;
+
+import android.graphics.RectF;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.widget.ImageView;
+
+/**
+ * Provided default implementation of GestureDetector.OnDoubleTapListener, to be overridden with custom behavior, if needed
+ * <p>&nbsp;</p>
+ * To be used via {@link uk.co.senab.photoview.PhotoViewAttacher#setOnDoubleTapListener(android.view.GestureDetector.OnDoubleTapListener)}
+ */
+public class DefaultOnDoubleTapListener implements GestureDetector.OnDoubleTapListener {
+
+    private PhotoViewAttacher photoViewAttacher;
+
+    /**
+     * Default constructor
+     *
+     * @param photoViewAttacher PhotoViewAttacher to bind to
+     */
+    public DefaultOnDoubleTapListener(PhotoViewAttacher photoViewAttacher) {
+        setPhotoViewAttacher(photoViewAttacher);
+    }
+
+    /**
+     * Allows to change PhotoViewAttacher within range of single instance
+     *
+     * @param newPhotoViewAttacher PhotoViewAttacher to bind to
+     */
+    public void setPhotoViewAttacher(PhotoViewAttacher newPhotoViewAttacher) {
+        this.photoViewAttacher = newPhotoViewAttacher;
+    }
+
+    @Override
+    public boolean onSingleTapConfirmed(MotionEvent e) {
+        if (this.photoViewAttacher == null)
+            return false;
+
+        ImageView imageView = photoViewAttacher.getImageView();
+
+        if (null != photoViewAttacher.getOnPhotoTapListener()) {
+            final RectF displayRect = photoViewAttacher.getDisplayRect();
+
+            if (null != displayRect) {
+                final float x = e.getX(), y = e.getY();
+
+                // Check to see if the user tapped on the photo
+                if (displayRect.contains(x, y)) {
+
+                    float xResult = (x - displayRect.left)
+                            / displayRect.width();
+                    float yResult = (y - displayRect.top)
+                            / displayRect.height();
+
+                    photoViewAttacher.getOnPhotoTapListener().onPhotoTap(imageView, xResult, yResult);
+                    return true;
+                }else{
+                    photoViewAttacher.getOnPhotoTapListener().onOutsidePhotoTap();
+                }
+            }
+        }
+        if (null != photoViewAttacher.getOnViewTapListener()) {
+            photoViewAttacher.getOnViewTapListener().onViewTap(imageView, e.getX(), e.getY());
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean onDoubleTap(MotionEvent ev) {
+        if (photoViewAttacher == null)
+            return false;
+
+        try {
+            float scale = photoViewAttacher.getScale();
+            float x = ev.getX();
+            float y = ev.getY();
+
+            if (scale < photoViewAttacher.getMediumScale()) {
+                photoViewAttacher.setScale(photoViewAttacher.getMediumScale(), x, y, true);
+            } else if (scale >= photoViewAttacher.getMediumScale() && scale < photoViewAttacher.getMaximumScale()) {
+                photoViewAttacher.setScale(photoViewAttacher.getMaximumScale(), x, y, true);
+            } else {
+                photoViewAttacher.setScale(photoViewAttacher.getMinimumScale(), x, y, true);
+            }
+        } catch (ArrayIndexOutOfBoundsException e) {
+            // Can sometimes happen when getX() and getY() is called
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean onDoubleTapEvent(MotionEvent e) {
+        // Wait for the confirmed onDoubleTap() instead
+        return false;
+    }
+
+}

+ 269 - 0
app/src/main/java/uk/co/senab/photoview/IPhotoView.java

@@ -0,0 +1,269 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.view.GestureDetector;
+import android.view.View;
+import android.widget.ImageView;
+
+
+public interface IPhotoView {
+
+    float DEFAULT_MAX_SCALE = 3.0f;
+    float DEFAULT_MID_SCALE = 1.75f;
+    float DEFAULT_MIN_SCALE = 1.0f;
+    int DEFAULT_ZOOM_DURATION = 200;
+
+    /**
+     * Returns true if the PhotoView is set to allow zooming of Photos.
+     *
+     * @return true if the PhotoView allows zooming.
+     */
+    boolean canZoom();
+
+    /**
+     * Gets the Display Rectangle of the currently displayed Drawable. The Rectangle is relative to
+     * this View and includes all scaling and translations.
+     *
+     * @return - RectF of Displayed Drawable
+     */
+    RectF getDisplayRect();
+
+    /**
+     * Sets the Display Matrix of the currently displayed Drawable. The Rectangle is considered
+     * relative to this View and includes all scaling and translations.
+     *
+     * @param finalMatrix target matrix to set PhotoView to
+     * @return - true if rectangle was applied successfully
+     */
+    boolean setDisplayMatrix(Matrix finalMatrix);
+
+    /**
+     * Copies the Display Matrix of the currently displayed Drawable. The Rectangle is considered
+     * relative to this View and includes all scaling and translations.
+     *
+     * @param matrix target matrix to copy to
+     */
+    void getDisplayMatrix(Matrix matrix);
+
+    /**
+     * @return The current minimum scale level. What this value represents depends on the current
+     * {@link android.widget.ImageView.ScaleType}.
+     */
+    float getMinimumScale();
+
+    /**
+     * @return The current medium scale level. What this value represents depends on the current
+     * {@link android.widget.ImageView.ScaleType}.
+     */
+    float getMediumScale();
+
+    /**
+     * @return The current maximum scale level. What this value represents depends on the current
+     * {@link android.widget.ImageView.ScaleType}.
+     */
+    float getMaximumScale();
+
+    /**
+     * Returns the current scale value
+     *
+     * @return float - current scale value
+     */
+    float getScale();
+
+    /**
+     * Return the current scale type in use by the ImageView.
+     *
+     * @return current ImageView.ScaleType
+     */
+    ImageView.ScaleType getScaleType();
+
+    /**
+     * Whether to allow the ImageView's parent to intercept the touch event when the photo is scroll
+     * to it's horizontal edge.
+     *
+     * @param allow whether to allow intercepting by parent element or not
+     */
+    void setAllowParentInterceptOnEdge(boolean allow);
+
+    /**
+     * Sets the minimum scale level. What this value represents depends on the current {@link
+     * android.widget.ImageView.ScaleType}.
+     *
+     * @param minimumScale minimum allowed scale
+     */
+    void setMinimumScale(float minimumScale);
+
+    /**
+     * Sets the medium scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}.
+     *
+     * @param mediumScale medium scale preset
+     */
+    void setMediumScale(float mediumScale);
+
+    /**
+     * Sets the maximum scale level. What this value represents depends on the current {@link
+     * android.widget.ImageView.ScaleType}.
+     *
+     * @param maximumScale maximum allowed scale preset
+     */
+    void setMaximumScale(float maximumScale);
+
+    /**
+     * Allows to set all three scale levels at once, so you don't run into problem with setting
+     * medium/minimum scale before the maximum one
+     *
+     * @param minimumScale minimum allowed scale
+     * @param mediumScale  medium allowed scale
+     * @param maximumScale maximum allowed scale preset
+     */
+    void setScaleLevels(float minimumScale, float mediumScale, float maximumScale);
+
+    /**
+     * Register a callback to be invoked when the Photo displayed by this view is long-pressed.
+     *
+     * @param listener - Listener to be registered.
+     */
+    void setOnLongClickListener(View.OnLongClickListener listener);
+
+    /**
+     * Register a callback to be invoked when the Matrix has changed for this View. An example would
+     * be the user panning or scaling the Photo.
+     *
+     * @param listener - Listener to be registered.
+     */
+    void setOnMatrixChangeListener(PhotoViewAttacher.OnMatrixChangedListener listener);
+
+    /**
+     * Register a callback to be invoked when the Photo displayed by this View is tapped with a
+     * single tap.
+     *
+     * @param listener - Listener to be registered.
+     */
+    void setOnPhotoTapListener(PhotoViewAttacher.OnPhotoTapListener listener);
+
+    /**
+     * Register a callback to be invoked when the View is tapped with a single tap.
+     *
+     * @param listener - Listener to be registered.
+     */
+    void setOnViewTapListener(PhotoViewAttacher.OnViewTapListener listener);
+
+    /**
+     * Enables rotation via PhotoView internal functions.
+     *
+     * @param rotationDegree - Degree to rotate PhotoView to, should be in range 0 to 360
+     */
+    void setRotationTo(float rotationDegree);
+
+    /**
+     * Enables rotation via PhotoView internal functions.
+     *
+     * @param rotationDegree - Degree to rotate PhotoView by, should be in range 0 to 360
+     */
+    void setRotationBy(float rotationDegree);
+
+    /**
+     * Changes the current scale to the specified value.
+     *
+     * @param scale - Value to scale to
+     */
+    void setScale(float scale);
+
+    /**
+     * Changes the current scale to the specified value.
+     *
+     * @param scale   - Value to scale to
+     * @param animate - Whether to animate the scale
+     */
+    void setScale(float scale, boolean animate);
+
+    /**
+     * Changes the current scale to the specified value, around the given focal point.
+     *
+     * @param scale   - Value to scale to
+     * @param focalX  - X Focus Point
+     * @param focalY  - Y Focus Point
+     * @param animate - Whether to animate the scale
+     */
+    void setScale(float scale, float focalX, float focalY, boolean animate);
+
+    /**
+     * Controls how the image should be resized or moved to match the size of the ImageView. Any
+     * scaling or panning will happen within the confines of this {@link
+     * android.widget.ImageView.ScaleType}.
+     *
+     * @param scaleType - The desired scaling mode.
+     */
+    void setScaleType(ImageView.ScaleType scaleType);
+
+    /**
+     * Allows you to enable/disable the zoom functionality on the ImageView. When disable the
+     * ImageView reverts to using the FIT_CENTER matrix.
+     *
+     * @param zoomable - Whether the zoom functionality is enabled.
+     */
+    void setZoomable(boolean zoomable);
+
+    /**
+     * Extracts currently visible area to Bitmap object, if there is no image loaded yet or the
+     * ImageView is already destroyed, returns {@code null}
+     *
+     * @return currently visible area as bitmap or null
+     */
+    Bitmap getVisibleRectangleBitmap();
+
+    /**
+     * Allows to change zoom transition speed, default value is 200 (PhotoViewAttacher.DEFAULT_ZOOM_DURATION).
+     * Will default to 200 if provided negative value
+     *
+     * @param milliseconds duration of zoom interpolation
+     */
+    void setZoomTransitionDuration(int milliseconds);
+
+    /**
+     * Will return instance of IPhotoView (eg. PhotoViewAttacher), can be used to provide better
+     * integration
+     *
+     * @return IPhotoView implementation instance if available, null if not
+     */
+    IPhotoView getIPhotoViewImplementation();
+
+    /**
+     * Sets custom double tap listener, to intercept default given functions. To reset behavior to
+     * default, you can just pass in "null" or public field of PhotoViewAttacher.defaultOnDoubleTapListener
+     *
+     * @param newOnDoubleTapListener custom OnDoubleTapListener to be set on ImageView
+     */
+    void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener);
+
+    /**
+     * Will report back about scale changes
+     *
+     * @param onScaleChangeListener OnScaleChangeListener instance
+     */
+    void setOnScaleChangeListener(PhotoViewAttacher.OnScaleChangeListener onScaleChangeListener);
+
+    /**
+     * Will report back about fling(single touch)
+     *
+     * @param onSingleFlingListener OnSingleFlingListener instance
+     */
+    void setOnSingleFlingListener(PhotoViewAttacher.OnSingleFlingListener onSingleFlingListener);
+}

+ 273 - 0
app/src/main/java/uk/co/senab/photoview/PhotoView.java

@@ -0,0 +1,273 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.widget.ImageView;
+
+import uk.co.senab.photoview.PhotoViewAttacher.OnMatrixChangedListener;
+import uk.co.senab.photoview.PhotoViewAttacher.OnPhotoTapListener;
+import uk.co.senab.photoview.PhotoViewAttacher.OnViewTapListener;
+
+public class PhotoView extends ImageView implements IPhotoView {
+
+    private PhotoViewAttacher mAttacher;
+
+    private ScaleType mPendingScaleType;
+
+    public PhotoView(Context context) {
+        this(context, null);
+    }
+
+    public PhotoView(Context context, AttributeSet attr) {
+        this(context, attr, 0);
+    }
+
+    public PhotoView(Context context, AttributeSet attr, int defStyle) {
+        super(context, attr, defStyle);
+        super.setScaleType(ScaleType.MATRIX);
+        init();
+    }
+
+    protected void init() {
+        if (null == mAttacher || null == mAttacher.getImageView()) {
+            mAttacher = new PhotoViewAttacher(this);
+        }
+
+        if (null != mPendingScaleType) {
+            setScaleType(mPendingScaleType);
+            mPendingScaleType = null;
+        }
+    }
+
+    @Override
+    public void setRotationTo(float rotationDegree) {
+        mAttacher.setRotationTo(rotationDegree);
+    }
+
+    @Override
+    public void setRotationBy(float rotationDegree) {
+        mAttacher.setRotationBy(rotationDegree);
+    }
+
+    @Override
+    public boolean canZoom() {
+        return mAttacher.canZoom();
+    }
+
+    @Override
+    public RectF getDisplayRect() {
+        return mAttacher.getDisplayRect();
+    }
+
+    @Override
+    public void getDisplayMatrix(Matrix matrix) {
+        mAttacher.getDisplayMatrix(matrix);
+    }
+
+    @Override
+    public boolean setDisplayMatrix(Matrix finalRectangle) {
+        return mAttacher.setDisplayMatrix(finalRectangle);
+    }
+
+    @Override
+    public float getMinimumScale() {
+        return mAttacher.getMinimumScale();
+    }
+
+    @Override
+    public float getMediumScale() {
+        return mAttacher.getMediumScale();
+    }
+
+    @Override
+    public float getMaximumScale() {
+        return mAttacher.getMaximumScale();
+    }
+
+    @Override
+    public float getScale() {
+        return mAttacher.getScale();
+    }
+
+    @Override
+    public ScaleType getScaleType() {
+        return mAttacher.getScaleType();
+    }
+
+    @Override
+    public Matrix getImageMatrix() {
+        return mAttacher.getImageMatrix();
+    }
+
+    @Override
+    public void setAllowParentInterceptOnEdge(boolean allow) {
+        mAttacher.setAllowParentInterceptOnEdge(allow);
+    }
+
+    @Override
+    public void setMinimumScale(float minimumScale) {
+        mAttacher.setMinimumScale(minimumScale);
+    }
+
+    @Override
+    public void setMediumScale(float mediumScale) {
+        mAttacher.setMediumScale(mediumScale);
+    }
+
+    @Override
+    public void setMaximumScale(float maximumScale) {
+        mAttacher.setMaximumScale(maximumScale);
+    }
+
+    @Override
+    public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) {
+        mAttacher.setScaleLevels(minimumScale, mediumScale, maximumScale);
+    }
+
+    @Override
+    // setImageBitmap calls through to this method
+    public void setImageDrawable(Drawable drawable) {
+        super.setImageDrawable(drawable);
+        if (null != mAttacher) {
+            mAttacher.update();
+        }
+    }
+
+    @Override
+    public void setImageResource(int resId) {
+        super.setImageResource(resId);
+        if (null != mAttacher) {
+            mAttacher.update();
+        }
+    }
+
+    @Override
+    public void setImageURI(Uri uri) {
+        super.setImageURI(uri);
+        if (null != mAttacher) {
+            mAttacher.update();
+        }
+    }
+
+    @Override
+    protected boolean setFrame(int l, int t, int r, int b) {
+        boolean changed = super.setFrame(l, t, r, b);
+        if (null != mAttacher) {
+            mAttacher.update();
+        }
+        return changed;
+    }
+
+    @Override
+    public void setOnMatrixChangeListener(OnMatrixChangedListener listener) {
+        mAttacher.setOnMatrixChangeListener(listener);
+    }
+
+    @Override
+    public void setOnLongClickListener(OnLongClickListener l) {
+        mAttacher.setOnLongClickListener(l);
+    }
+
+    @Override
+    public void setOnPhotoTapListener(OnPhotoTapListener listener) {
+        mAttacher.setOnPhotoTapListener(listener);
+    }
+
+    @Override
+    public void setOnViewTapListener(OnViewTapListener listener) {
+        mAttacher.setOnViewTapListener(listener);
+    }
+
+    @Override
+    public void setScale(float scale) {
+        mAttacher.setScale(scale);
+    }
+
+    @Override
+    public void setScale(float scale, boolean animate) {
+        mAttacher.setScale(scale, animate);
+    }
+
+    @Override
+    public void setScale(float scale, float focalX, float focalY, boolean animate) {
+        mAttacher.setScale(scale, focalX, focalY, animate);
+    }
+
+    @Override
+    public void setScaleType(ScaleType scaleType) {
+        if (null != mAttacher) {
+            mAttacher.setScaleType(scaleType);
+        } else {
+            mPendingScaleType = scaleType;
+        }
+    }
+
+    @Override
+    public void setZoomable(boolean zoomable) {
+        mAttacher.setZoomable(zoomable);
+    }
+
+    @Override
+    public Bitmap getVisibleRectangleBitmap() {
+        return mAttacher.getVisibleRectangleBitmap();
+    }
+
+    @Override
+    public void setZoomTransitionDuration(int milliseconds) {
+        mAttacher.setZoomTransitionDuration(milliseconds);
+    }
+
+    @Override
+    public IPhotoView getIPhotoViewImplementation() {
+        return mAttacher;
+    }
+
+    @Override
+    public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) {
+        mAttacher.setOnDoubleTapListener(newOnDoubleTapListener);
+    }
+
+    @Override
+    public void setOnScaleChangeListener(PhotoViewAttacher.OnScaleChangeListener onScaleChangeListener) {
+        mAttacher.setOnScaleChangeListener(onScaleChangeListener);
+    }
+
+    @Override
+    public void setOnSingleFlingListener(PhotoViewAttacher.OnSingleFlingListener onSingleFlingListener) {
+        mAttacher.setOnSingleFlingListener(onSingleFlingListener);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        mAttacher.cleanup();
+        mAttacher = null;
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        init();
+        super.onAttachedToWindow();
+    }
+}

+ 1192 - 0
app/src/main/java/uk/co/senab/photoview/PhotoViewAttacher.java

@@ -0,0 +1,1192 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.Matrix.ScaleToFit;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.support.v4.view.MotionEventCompat;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnLongClickListener;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+
+import java.lang.ref.WeakReference;
+
+import uk.co.senab.photoview.gestures.OnGestureListener;
+import uk.co.senab.photoview.gestures.VersionedGestureDetector;
+import uk.co.senab.photoview.log.LogManager;
+import uk.co.senab.photoview.scrollerproxy.ScrollerProxy;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+
+public class PhotoViewAttacher implements IPhotoView, View.OnTouchListener,
+        OnGestureListener,
+        ViewTreeObserver.OnGlobalLayoutListener {
+
+    private static final String LOG_TAG = "PhotoViewAttacher";
+
+    // let debug flag be dynamic, but still Proguard can be used to remove from
+    // release builds
+    private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
+
+    private Interpolator mInterpolator = new AccelerateDecelerateInterpolator();
+    int ZOOM_DURATION = DEFAULT_ZOOM_DURATION;
+
+    static final int EDGE_NONE = -1;
+    static final int EDGE_LEFT = 0;
+    static final int EDGE_RIGHT = 1;
+    static final int EDGE_BOTH = 2;
+
+    static int SINGLE_TOUCH = 1;
+
+    private float mMinScale = DEFAULT_MIN_SCALE;
+    private float mMidScale = DEFAULT_MID_SCALE;
+    private float mMaxScale = DEFAULT_MAX_SCALE;
+
+    private boolean mAllowParentInterceptOnEdge = true;
+    private boolean mBlockParentIntercept = false;
+
+    private static void checkZoomLevels(float minZoom, float midZoom,
+                                        float maxZoom) {
+        if (minZoom >= midZoom) {
+            throw new IllegalArgumentException(
+                    "Minimum zoom has to be less than Medium zoom. Call setMinimumZoom() with a more appropriate value");
+        } else if (midZoom >= maxZoom) {
+            throw new IllegalArgumentException(
+                    "Medium zoom has to be less than Maximum zoom. Call setMaximumZoom() with a more appropriate value");
+        }
+    }
+
+    /**
+     * @return true if the ImageView exists, and it's Drawable exists
+     */
+    private static boolean hasDrawable(ImageView imageView) {
+        return null != imageView && null != imageView.getDrawable();
+    }
+
+    /**
+     * @return true if the ScaleType is supported.
+     */
+    private static boolean isSupportedScaleType(final ScaleType scaleType) {
+        if (null == scaleType) {
+            return false;
+        }
+
+        switch (scaleType) {
+            case MATRIX:
+                throw new IllegalArgumentException(scaleType.name()
+                        + " is not supported in PhotoView");
+
+            default:
+                return true;
+        }
+    }
+
+    /**
+     * Set's the ImageView's ScaleType to Matrix.
+     */
+    private static void setImageViewScaleTypeMatrix(ImageView imageView) {
+        /**
+         * PhotoView sets it's own ScaleType to Matrix, then diverts all calls
+         * setScaleType to this.setScaleType automatically.
+         */
+        if (null != imageView && !(imageView instanceof IPhotoView)) {
+            if (!ScaleType.MATRIX.equals(imageView.getScaleType())) {
+                imageView.setScaleType(ScaleType.MATRIX);
+            }
+        }
+    }
+
+    private WeakReference<ImageView> mImageView;
+
+    // Gesture Detectors
+    private GestureDetector mGestureDetector;
+    private uk.co.senab.photoview.gestures.GestureDetector mScaleDragDetector;
+
+    // These are set so we don't keep allocating them on the heap
+    private final Matrix mBaseMatrix = new Matrix();
+    private final Matrix mDrawMatrix = new Matrix();
+    private final Matrix mSuppMatrix = new Matrix();
+    private final RectF mDisplayRect = new RectF();
+    private final float[] mMatrixValues = new float[9];
+
+    // Listeners
+    private OnMatrixChangedListener mMatrixChangeListener;
+    private OnPhotoTapListener mPhotoTapListener;
+    private OnViewTapListener mViewTapListener;
+    private OnLongClickListener mLongClickListener;
+    private OnScaleChangeListener mScaleChangeListener;
+    private OnSingleFlingListener mSingleFlingListener;
+
+    private int mIvTop, mIvRight, mIvBottom, mIvLeft;
+    private FlingRunnable mCurrentFlingRunnable;
+    private int mScrollEdge = EDGE_BOTH;
+    private float mBaseRotation;
+
+    private boolean mZoomEnabled;
+    private ScaleType mScaleType = ScaleType.FIT_CENTER;
+
+    public PhotoViewAttacher(ImageView imageView) {
+        this(imageView, true);
+    }
+
+    public PhotoViewAttacher(ImageView imageView, boolean zoomable) {
+        mImageView = new WeakReference<>(imageView);
+
+        imageView.setDrawingCacheEnabled(true);
+        imageView.setOnTouchListener(this);
+
+        ViewTreeObserver observer = imageView.getViewTreeObserver();
+        if (null != observer)
+            observer.addOnGlobalLayoutListener(this);
+
+        // Make sure we using MATRIX Scale Type
+        setImageViewScaleTypeMatrix(imageView);
+
+        if (imageView.isInEditMode()) {
+            return;
+        }
+        // Create Gesture Detectors...
+        mScaleDragDetector = VersionedGestureDetector.newInstance(
+                imageView.getContext(), this);
+
+        mGestureDetector = new GestureDetector(imageView.getContext(),
+                new GestureDetector.SimpleOnGestureListener() {
+
+                    // forward long click listener
+                    @Override
+                    public void onLongPress(MotionEvent e) {
+                        if (null != mLongClickListener) {
+                            mLongClickListener.onLongClick(getImageView());
+                        }
+                    }
+
+                    @Override
+                    public boolean onFling(MotionEvent e1, MotionEvent e2,
+                                           float velocityX, float velocityY) {
+                        if (mSingleFlingListener != null) {
+                            if (getScale() > DEFAULT_MIN_SCALE) {
+                                return false;
+                            }
+
+                            if (MotionEventCompat.getPointerCount(e1) > SINGLE_TOUCH
+                                    || MotionEventCompat.getPointerCount(e2) > SINGLE_TOUCH) {
+                                return false;
+                            }
+
+                            return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY);
+                        }
+                        return false;
+                    }
+                });
+
+        mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));
+        mBaseRotation = 0.0f;
+
+        // Finally, update the UI so that we're zoomable
+        setZoomable(zoomable);
+    }
+
+    @Override
+    public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) {
+        if (newOnDoubleTapListener != null) {
+            this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener);
+        } else {
+            this.mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));
+        }
+    }
+
+    @Override
+    public void setOnScaleChangeListener(OnScaleChangeListener onScaleChangeListener) {
+        this.mScaleChangeListener = onScaleChangeListener;
+    }
+
+    @Override
+    public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) {
+        this.mSingleFlingListener = onSingleFlingListener;
+    }
+
+    @Override
+    public boolean canZoom() {
+        return mZoomEnabled;
+    }
+
+    /**
+     * Clean-up the resources attached to this object. This needs to be called when the ImageView is
+     * no longer used. A good example is from {@link android.view.View#onDetachedFromWindow()} or
+     * from {@link android.app.Activity#onDestroy()}. This is automatically called if you are using
+     * {@link uk.co.senab.photoview.PhotoView}.
+     */
+    @SuppressWarnings("deprecation")
+    public void cleanup() {
+        if (null == mImageView) {
+            return; // cleanup already done
+        }
+
+        final ImageView imageView = mImageView.get();
+
+        if (null != imageView) {
+            // Remove this as a global layout listener
+            ViewTreeObserver observer = imageView.getViewTreeObserver();
+            if (null != observer && observer.isAlive()) {
+                observer.removeGlobalOnLayoutListener(this);
+            }
+
+            // Remove the ImageView's reference to this
+            imageView.setOnTouchListener(null);
+
+            // make sure a pending fling runnable won't be run
+            cancelFling();
+        }
+
+        if (null != mGestureDetector) {
+            mGestureDetector.setOnDoubleTapListener(null);
+        }
+
+        // Clear listeners too
+        mMatrixChangeListener = null;
+        mPhotoTapListener = null;
+        mViewTapListener = null;
+
+        // Finally, clear ImageView
+        mImageView = null;
+    }
+
+    @Override
+    public RectF getDisplayRect() {
+        checkMatrixBounds();
+        return getDisplayRect(getDrawMatrix());
+    }
+
+    @Override
+    public boolean setDisplayMatrix(Matrix finalMatrix) {
+        if (finalMatrix == null) {
+            throw new IllegalArgumentException("Matrix cannot be null");
+        }
+
+        ImageView imageView = getImageView();
+        if (null == imageView) {
+            return false;
+        }
+
+        if (null == imageView.getDrawable()) {
+            return false;
+        }
+
+        mSuppMatrix.set(finalMatrix);
+        setImageViewMatrix(getDrawMatrix());
+        checkMatrixBounds();
+
+        return true;
+    }
+
+    public void setBaseRotation(final float degrees) {
+        mBaseRotation = degrees % 360;
+        update();
+        setRotationBy(mBaseRotation);
+        checkAndDisplayMatrix();
+    }
+
+    @Override
+    public void setRotationTo(float degrees) {
+        mSuppMatrix.setRotate(degrees % 360);
+        checkAndDisplayMatrix();
+    }
+
+    @Override
+    public void setRotationBy(float degrees) {
+        mSuppMatrix.postRotate(degrees % 360);
+        checkAndDisplayMatrix();
+    }
+
+    public ImageView getImageView() {
+        ImageView imageView = null;
+
+        if (null != mImageView) {
+            imageView = mImageView.get();
+        }
+
+        // If we don't have an ImageView, call cleanup()
+        if (null == imageView) {
+            cleanup();
+            LogManager.getLogger().i(LOG_TAG,
+                    "ImageView no longer exists. You should not use this PhotoViewAttacher any more.");
+        }
+
+        return imageView;
+    }
+
+    @Override
+    public float getMinimumScale() {
+        return mMinScale;
+    }
+
+    @Override
+    public float getMediumScale() {
+        return mMidScale;
+    }
+
+    @Override
+    public float getMaximumScale() {
+        return mMaxScale;
+    }
+
+    @Override
+    public float getScale() {
+        return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow(getValue(mSuppMatrix, Matrix.MSKEW_Y), 2));
+    }
+
+    @Override
+    public ScaleType getScaleType() {
+        return mScaleType;
+    }
+
+    @Override
+    public void onDrag(float dx, float dy) {
+        if (mScaleDragDetector.isScaling()) {
+            return; // Do not drag if we are already scaling
+        }
+
+        if (DEBUG) {
+            LogManager.getLogger().d(LOG_TAG,
+                    String.format("onDrag: dx: %.2f. dy: %.2f", dx, dy));
+        }
+
+        ImageView imageView = getImageView();
+        mSuppMatrix.postTranslate(dx, dy);
+        checkAndDisplayMatrix();
+
+        /**
+         * Here we decide whether to let the ImageView's parent to start taking
+         * over the touch event.
+         *
+         * First we check whether this function is enabled. We never want the
+         * parent to take over if we're scaling. We then check the edge we're
+         * on, and the direction of the scroll (i.e. if we're pulling against
+         * the edge, aka 'overscrolling', let the parent take over).
+         */
+        ViewParent parent = imageView.getParent();
+        if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {
+            if (mScrollEdge == EDGE_BOTH
+                    || (mScrollEdge == EDGE_LEFT && dx >= 1f)
+                    || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) {
+                if (null != parent) {
+                    parent.requestDisallowInterceptTouchEvent(false);
+                }
+            }
+        } else {
+            if (null != parent) {
+                parent.requestDisallowInterceptTouchEvent(true);
+            }
+        }
+    }
+
+    @Override
+    public void onFling(float startX, float startY, float velocityX,
+                        float velocityY) {
+        if (DEBUG) {
+            LogManager.getLogger().d(
+                    LOG_TAG,
+                    "onFling. sX: " + startX + " sY: " + startY + " Vx: "
+                            + velocityX + " Vy: " + velocityY);
+        }
+        ImageView imageView = getImageView();
+        mCurrentFlingRunnable = new FlingRunnable(imageView.getContext());
+        mCurrentFlingRunnable.fling(getImageViewWidth(imageView),
+                getImageViewHeight(imageView), (int) velocityX, (int) velocityY);
+        imageView.post(mCurrentFlingRunnable);
+    }
+
+    @Override
+    public void onGlobalLayout() {
+        ImageView imageView = getImageView();
+
+        if (null != imageView) {
+            if (mZoomEnabled) {
+                final int top = imageView.getTop();
+                final int right = imageView.getRight();
+                final int bottom = imageView.getBottom();
+                final int left = imageView.getLeft();
+
+                /**
+                 * We need to check whether the ImageView's bounds have changed.
+                 * This would be easier if we targeted API 11+ as we could just use
+                 * View.OnLayoutChangeListener. Instead we have to replicate the
+                 * work, keeping track of the ImageView's bounds and then checking
+                 * if the values change.
+                 */
+                if (top != mIvTop || bottom != mIvBottom || left != mIvLeft
+                        || right != mIvRight) {
+                    // Update our base matrix, as the bounds have changed
+                    updateBaseMatrix(imageView.getDrawable());
+
+                    // Update values as something has changed
+                    mIvTop = top;
+                    mIvRight = right;
+                    mIvBottom = bottom;
+                    mIvLeft = left;
+                }
+            } else {
+                updateBaseMatrix(imageView.getDrawable());
+            }
+        }
+    }
+
+    @Override
+    public void onScale(float scaleFactor, float focusX, float focusY) {
+        if (DEBUG) {
+            LogManager.getLogger().d(
+                    LOG_TAG,
+                    String.format("onScale: scale: %.2f. fX: %.2f. fY: %.2f",
+                            scaleFactor, focusX, focusY));
+        }
+
+        if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) {
+            if (null != mScaleChangeListener) {
+                mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
+            }
+            mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
+            checkAndDisplayMatrix();
+        }
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    @Override
+    public boolean onTouch(View v, MotionEvent ev) {
+        boolean handled = false;
+
+        if (mZoomEnabled && hasDrawable((ImageView) v)) {
+            ViewParent parent = v.getParent();
+            switch (ev.getAction()) {
+                case ACTION_DOWN:
+                    // First, disable the Parent from intercepting the touch
+                    // event
+                    if (null != parent) {
+                        parent.requestDisallowInterceptTouchEvent(true);
+                    } else {
+                        LogManager.getLogger().i(LOG_TAG, "onTouch getParent() returned null");
+                    }
+
+                    // If we're flinging, and the user presses down, cancel
+                    // fling
+                    cancelFling();
+                    break;
+
+                case ACTION_CANCEL:
+                case ACTION_UP:
+                    // If the user has zoomed less than min scale, zoom back
+                    // to min scale
+                    if (getScale() < mMinScale) {
+                        RectF rect = getDisplayRect();
+                        if (null != rect) {
+                            v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
+                                    rect.centerX(), rect.centerY()));
+                            handled = true;
+                        }
+                    }
+                    break;
+            }
+
+            // Try the Scale/Drag detector
+            if (null != mScaleDragDetector) {
+                boolean wasScaling = mScaleDragDetector.isScaling();
+                boolean wasDragging = mScaleDragDetector.isDragging();
+
+                handled = mScaleDragDetector.onTouchEvent(ev);
+
+                boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();
+                boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();
+
+                mBlockParentIntercept = didntScale && didntDrag;
+            }
+
+            // Check to see if the user double tapped
+            if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) {
+                handled = true;
+            }
+
+        }
+
+        return handled;
+    }
+
+    @Override
+    public void setAllowParentInterceptOnEdge(boolean allow) {
+        mAllowParentInterceptOnEdge = allow;
+    }
+
+    @Override
+    public void setMinimumScale(float minimumScale) {
+        checkZoomLevels(minimumScale, mMidScale, mMaxScale);
+        mMinScale = minimumScale;
+    }
+
+    @Override
+    public void setMediumScale(float mediumScale) {
+        checkZoomLevels(mMinScale, mediumScale, mMaxScale);
+        mMidScale = mediumScale;
+    }
+
+    @Override
+    public void setMaximumScale(float maximumScale) {
+        checkZoomLevels(mMinScale, mMidScale, maximumScale);
+        mMaxScale = maximumScale;
+    }
+
+    @Override
+    public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) {
+        checkZoomLevels(minimumScale, mediumScale, maximumScale);
+        mMinScale = minimumScale;
+        mMidScale = mediumScale;
+        mMaxScale = maximumScale;
+    }
+
+    @Override
+    public void setOnLongClickListener(OnLongClickListener listener) {
+        mLongClickListener = listener;
+    }
+
+    @Override
+    public void setOnMatrixChangeListener(OnMatrixChangedListener listener) {
+        mMatrixChangeListener = listener;
+    }
+
+    @Override
+    public void setOnPhotoTapListener(OnPhotoTapListener listener) {
+        mPhotoTapListener = listener;
+    }
+
+    @Nullable
+    OnPhotoTapListener getOnPhotoTapListener() {
+        return mPhotoTapListener;
+    }
+
+    @Override
+    public void setOnViewTapListener(OnViewTapListener listener) {
+        mViewTapListener = listener;
+    }
+
+    @Nullable
+    OnViewTapListener getOnViewTapListener() {
+        return mViewTapListener;
+    }
+
+    @Override
+    public void setScale(float scale) {
+        setScale(scale, false);
+    }
+
+    @Override
+    public void setScale(float scale, boolean animate) {
+        ImageView imageView = getImageView();
+
+        if (null != imageView) {
+            setScale(scale,
+                    (imageView.getRight()) / 2,
+                    (imageView.getBottom()) / 2,
+                    animate);
+        }
+    }
+
+    @Override
+    public void setScale(float scale, float focalX, float focalY,
+                         boolean animate) {
+        ImageView imageView = getImageView();
+
+        if (null != imageView) {
+            // Check to see if the scale is within bounds
+            if (scale < mMinScale || scale > mMaxScale) {
+                LogManager
+                        .getLogger()
+                        .i(LOG_TAG,
+                                "Scale must be within the range of minScale and maxScale");
+                return;
+            }
+
+            if (animate) {
+                imageView.post(new AnimatedZoomRunnable(getScale(), scale,
+                        focalX, focalY));
+            } else {
+                mSuppMatrix.setScale(scale, scale, focalX, focalY);
+                checkAndDisplayMatrix();
+            }
+        }
+    }
+
+    /**
+     * Set the zoom interpolator
+     * @param interpolator the zoom interpolator
+     */
+    public void setZoomInterpolator(Interpolator interpolator) {
+        mInterpolator = interpolator;
+    }
+
+    @Override
+    public void setScaleType(ScaleType scaleType) {
+        if (isSupportedScaleType(scaleType) && scaleType != mScaleType) {
+            mScaleType = scaleType;
+
+            // Finally update
+            update();
+        }
+    }
+
+    @Override
+    public void setZoomable(boolean zoomable) {
+        mZoomEnabled = zoomable;
+        update();
+    }
+
+    public void update() {
+        ImageView imageView = getImageView();
+
+        if (null != imageView) {
+            if (mZoomEnabled) {
+                // Make sure we using MATRIX Scale Type
+                setImageViewScaleTypeMatrix(imageView);
+
+                // Update the base matrix using the current drawable
+                updateBaseMatrix(imageView.getDrawable());
+            } else {
+                // Reset the Matrix...
+                resetMatrix();
+            }
+        }
+    }
+
+    /**
+     * Get the display matrix
+     * @param matrix target matrix to copy to
+     */
+    @Override
+    public void getDisplayMatrix(Matrix matrix) {
+        matrix.set(getDrawMatrix());
+    }
+
+    /**
+     * Get the current support matrix
+     */
+    public void getSuppMatrix(Matrix matrix) {
+        matrix.set(mSuppMatrix);
+    }
+
+    private Matrix getDrawMatrix() {
+        mDrawMatrix.set(mBaseMatrix);
+        mDrawMatrix.postConcat(mSuppMatrix);
+        return mDrawMatrix;
+    }
+
+    private void cancelFling() {
+        if (null != mCurrentFlingRunnable) {
+            mCurrentFlingRunnable.cancelFling();
+            mCurrentFlingRunnable = null;
+        }
+    }
+
+    public Matrix getImageMatrix() {
+        return mDrawMatrix;
+    }
+
+    /**
+     * Helper method that simply checks the Matrix, and then displays the result
+     */
+    private void checkAndDisplayMatrix() {
+        if (checkMatrixBounds()) {
+            setImageViewMatrix(getDrawMatrix());
+        }
+    }
+
+    private void checkImageViewScaleType() {
+        ImageView imageView = getImageView();
+
+        /**
+         * PhotoView's getScaleType() will just divert to this.getScaleType() so
+         * only call if we're not attached to a PhotoView.
+         */
+        if (null != imageView && !(imageView instanceof IPhotoView)) {
+            if (!ScaleType.MATRIX.equals(imageView.getScaleType())) {
+                throw new IllegalStateException(
+                        "The ImageView's ScaleType has been changed since attaching a PhotoViewAttacher. You should call setScaleType on the PhotoViewAttacher instead of on the ImageView"  );
+            }
+        }
+    }
+
+    private boolean checkMatrixBounds() {
+        final ImageView imageView = getImageView();
+        if (null == imageView) {
+            return false;
+        }
+
+        final RectF rect = getDisplayRect(getDrawMatrix());
+        if (null == rect) {
+            return false;
+        }
+
+        final float height = rect.height(), width = rect.width();
+        float deltaX = 0, deltaY = 0;
+
+        final int viewHeight = getImageViewHeight(imageView);
+        if (height <= viewHeight) {
+            switch (mScaleType) {
+                case FIT_START:
+                    deltaY = -rect.top;
+                    break;
+                case FIT_END:
+                    deltaY = viewHeight - height - rect.top;
+                    break;
+                default:
+                    deltaY = (viewHeight - height) / 2 - rect.top;
+                    break;
+            }
+        } else if (rect.top > 0) {
+            deltaY = -rect.top;
+        } else if (rect.bottom < viewHeight) {
+            deltaY = viewHeight - rect.bottom;
+        }
+
+        final int viewWidth = getImageViewWidth(imageView);
+        if (width <= viewWidth) {
+            switch (mScaleType) {
+                case FIT_START:
+                    deltaX = -rect.left;
+                    break;
+                case FIT_END:
+                    deltaX = viewWidth - width - rect.left;
+                    break;
+                default:
+                    deltaX = (viewWidth - width) / 2 - rect.left;
+                    break;
+            }
+            mScrollEdge = EDGE_BOTH;
+        } else if (rect.left > 0) {
+            mScrollEdge = EDGE_LEFT;
+            deltaX = -rect.left;
+        } else if (rect.right < viewWidth) {
+            deltaX = viewWidth - rect.right;
+            mScrollEdge = EDGE_RIGHT;
+        } else {
+            mScrollEdge = EDGE_NONE;
+        }
+
+        // Finally actually translate the matrix
+        mSuppMatrix.postTranslate(deltaX, deltaY);
+        return true;
+    }
+
+    /**
+     * Helper method that maps the supplied Matrix to the current Drawable
+     *
+     * @param matrix - Matrix to map Drawable against
+     * @return RectF - Displayed Rectangle
+     */
+    private RectF getDisplayRect(Matrix matrix) {
+        ImageView imageView = getImageView();
+
+        if (null != imageView) {
+            Drawable d = imageView.getDrawable();
+            if (null != d) {
+                mDisplayRect.set(0, 0, d.getIntrinsicWidth(),
+                        d.getIntrinsicHeight());
+                matrix.mapRect(mDisplayRect);
+                return mDisplayRect;
+            }
+        }
+        return null;
+    }
+
+    public Bitmap getVisibleRectangleBitmap() {
+        ImageView imageView = getImageView();
+        return imageView == null ? null : imageView.getDrawingCache();
+    }
+
+    @Override
+    public void setZoomTransitionDuration(int milliseconds) {
+        if (milliseconds < 0)
+            milliseconds = DEFAULT_ZOOM_DURATION;
+        this.ZOOM_DURATION = milliseconds;
+    }
+
+    @Override
+    public IPhotoView getIPhotoViewImplementation() {
+        return this;
+    }
+
+    /**
+     * Helper method that 'unpacks' a Matrix and returns the required value
+     *
+     * @param matrix     - Matrix to unpack
+     * @param whichValue - Which value from Matrix.M* to return
+     * @return float - returned value
+     */
+    private float getValue(Matrix matrix, int whichValue) {
+        matrix.getValues(mMatrixValues);
+        return mMatrixValues[whichValue];
+    }
+
+    /**
+     * Resets the Matrix back to FIT_CENTER, and then displays it.s
+     */
+    private void resetMatrix() {
+        mSuppMatrix.reset();
+        setRotationBy(mBaseRotation);
+        setImageViewMatrix(getDrawMatrix());
+        checkMatrixBounds();
+    }
+
+    private void setImageViewMatrix(Matrix matrix) {
+        ImageView imageView = getImageView();
+        if (null != imageView) {
+
+            checkImageViewScaleType();
+            imageView.setImageMatrix(matrix);
+
+            // Call MatrixChangedListener if needed
+            if (null != mMatrixChangeListener) {
+                RectF displayRect = getDisplayRect(matrix);
+                if (null != displayRect) {
+                    mMatrixChangeListener.onMatrixChanged(displayRect);
+                }
+            }
+        }
+    }
+
+    /**
+     * Calculate Matrix for FIT_CENTER
+     *
+     * @param d - Drawable being displayed
+     */
+    private void updateBaseMatrix(Drawable d) {
+        ImageView imageView = getImageView();
+        if (null == imageView || null == d) {
+            return;
+        }
+
+        final float viewWidth = getImageViewWidth(imageView);
+        final float viewHeight = getImageViewHeight(imageView);
+        final int drawableWidth = d.getIntrinsicWidth();
+        final int drawableHeight = d.getIntrinsicHeight();
+
+        mBaseMatrix.reset();
+
+        final float widthScale = viewWidth / drawableWidth;
+        final float heightScale = viewHeight / drawableHeight;
+
+        if (mScaleType == ScaleType.CENTER) {
+            mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F,
+                    (viewHeight - drawableHeight) / 2F);
+
+        } else if (mScaleType == ScaleType.CENTER_CROP) {
+            float scale = Math.max(widthScale, heightScale);
+            mBaseMatrix.postScale(scale, scale);
+            mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
+                    (viewHeight - drawableHeight * scale) / 2F);
+
+        } else if (mScaleType == ScaleType.CENTER_INSIDE) {
+            float scale = Math.min(1.0f, Math.min(widthScale, heightScale));
+            mBaseMatrix.postScale(scale, scale);
+            mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
+                    (viewHeight - drawableHeight * scale) / 2F);
+
+        } else {
+            RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight);
+            RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);
+
+            if ((int) mBaseRotation % 180 != 0) {
+                mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth);
+            }
+
+            switch (mScaleType) {
+                case FIT_CENTER:
+                    mBaseMatrix
+                            .setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER);
+                    break;
+
+                case FIT_START:
+                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START);
+                    break;
+
+                case FIT_END:
+                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END);
+                    break;
+
+                case FIT_XY:
+                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL);
+                    break;
+
+                default:
+                    break;
+            }
+        }
+
+        resetMatrix();
+    }
+
+    private int getImageViewWidth(ImageView imageView) {
+        if (null == imageView)
+            return 0;
+        return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight();
+    }
+
+    private int getImageViewHeight(ImageView imageView) {
+        if (null == imageView)
+            return 0;
+        return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom();
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when the internal Matrix has changed for
+     * this View.
+     *
+     * @author Chris Banes
+     */
+    public interface OnMatrixChangedListener {
+        /**
+         * Callback for when the Matrix displaying the Drawable has changed. This could be because
+         * the View's bounds have changed, or the user has zoomed.
+         *
+         * @param rect - Rectangle displaying the Drawable's new bounds.
+         */
+        void onMatrixChanged(RectF rect);
+    }
+
+    /**
+     * Interface definition for callback to be invoked when attached ImageView scale changes
+     *
+     * @author Marek Sebera
+     */
+    public interface OnScaleChangeListener {
+        /**
+         * Callback for when the scale changes
+         *
+         * @param scaleFactor the scale factor (less than 1 for zoom out, greater than 1 for zoom in)
+         * @param focusX      focal point X position
+         * @param focusY      focal point Y position
+         */
+        void onScaleChange(float scaleFactor, float focusX, float focusY);
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when the Photo is tapped with a single
+     * tap.
+     *
+     * @author Chris Banes
+     */
+    public interface OnPhotoTapListener {
+
+        /**
+         * A callback to receive where the user taps on a photo. You will only receive a callback if
+         * the user taps on the actual photo, tapping on 'whitespace' will be ignored.
+         *
+         * @param view - View the user tapped.
+         * @param x    - where the user tapped from the of the Drawable, as percentage of the
+         *             Drawable width.
+         * @param y    - where the user tapped from the top of the Drawable, as percentage of the
+         *             Drawable height.
+         */
+        void onPhotoTap(View view, float x, float y);
+
+        /**
+         * A simple callback where out of photo happened;
+         * */
+        void onOutsidePhotoTap();
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when the ImageView is tapped with a single
+     * tap.
+     *
+     * @author Chris Banes
+     */
+    public interface OnViewTapListener {
+
+        /**
+         * A callback to receive where the user taps on a ImageView. You will receive a callback if
+         * the user taps anywhere on the view, tapping on 'whitespace' will not be ignored.
+         *
+         * @param view - View the user tapped.
+         * @param x    - where the user tapped from the left of the View.
+         * @param y    - where the user tapped from the top of the View.
+         */
+        void onViewTap(View view, float x, float y);
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when the ImageView is fling with a single
+     * touch
+     *
+     * @author tonyjs
+     */
+    public interface OnSingleFlingListener {
+
+        /**
+         * A callback to receive where the user flings on a ImageView. You will receive a callback if
+         * the user flings anywhere on the view.
+         *
+         * @param e1        - MotionEvent the user first touch.
+         * @param e2        - MotionEvent the user last touch.
+         * @param velocityX - distance of user's horizontal fling.
+         * @param velocityY - distance of user's vertical fling.
+         */
+        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
+    }
+
+    private class AnimatedZoomRunnable implements Runnable {
+
+        private final float mFocalX, mFocalY;
+        private final long mStartTime;
+        private final float mZoomStart, mZoomEnd;
+
+        public AnimatedZoomRunnable(final float currentZoom, final float targetZoom,
+                                    final float focalX, final float focalY) {
+            mFocalX = focalX;
+            mFocalY = focalY;
+            mStartTime = System.currentTimeMillis();
+            mZoomStart = currentZoom;
+            mZoomEnd = targetZoom;
+        }
+
+        @Override
+        public void run() {
+            ImageView imageView = getImageView();
+            if (imageView == null) {
+                return;
+            }
+
+            float t = interpolate();
+            float scale = mZoomStart + t * (mZoomEnd - mZoomStart);
+            float deltaScale = scale / getScale();
+
+            onScale(deltaScale, mFocalX, mFocalY);
+
+            // We haven't hit our target scale yet, so post ourselves again
+            if (t < 1f) {
+                Compat.postOnAnimation(imageView, this);
+            }
+        }
+
+        private float interpolate() {
+            float t = 1f * (System.currentTimeMillis() - mStartTime) / ZOOM_DURATION;
+            t = Math.min(1f, t);
+            t = mInterpolator.getInterpolation(t);
+            return t;
+        }
+    }
+
+    private class FlingRunnable implements Runnable {
+
+        private final ScrollerProxy mScroller;
+        private int mCurrentX, mCurrentY;
+
+        public FlingRunnable(Context context) {
+            mScroller = ScrollerProxy.getScroller(context);
+        }
+
+        public void cancelFling() {
+            if (DEBUG) {
+                LogManager.getLogger().d(LOG_TAG, "Cancel Fling");
+            }
+            mScroller.forceFinished(true);
+        }
+
+        public void fling(int viewWidth, int viewHeight, int velocityX,
+                          int velocityY) {
+            final RectF rect = getDisplayRect();
+            if (null == rect) {
+                return;
+            }
+
+            final int startX = Math.round(-rect.left);
+            final int minX, maxX, minY, maxY;
+
+            if (viewWidth < rect.width()) {
+                minX = 0;
+                maxX = Math.round(rect.width() - viewWidth);
+            } else {
+                minX = maxX = startX;
+            }
+
+            final int startY = Math.round(-rect.top);
+            if (viewHeight < rect.height()) {
+                minY = 0;
+                maxY = Math.round(rect.height() - viewHeight);
+            } else {
+                minY = maxY = startY;
+            }
+
+            mCurrentX = startX;
+            mCurrentY = startY;
+
+            if (DEBUG) {
+                LogManager.getLogger().d(
+                        LOG_TAG,
+                        "fling. StartX:" + startX + " StartY:" + startY
+                                + " MaxX:" + maxX + " MaxY:" + maxY);
+            }
+
+            // If we actually can move, fling the scroller
+            if (startX != maxX || startY != maxY) {
+                mScroller.fling(startX, startY, velocityX, velocityY, minX,
+                        maxX, minY, maxY, 0, 0);
+            }
+        }
+
+        @Override
+        public void run() {
+            if (mScroller.isFinished()) {
+                return; // remaining post that should not be handled
+            }
+
+            ImageView imageView = getImageView();
+            if (null != imageView && mScroller.computeScrollOffset()) {
+
+                final int newX = mScroller.getCurrX();
+                final int newY = mScroller.getCurrY();
+
+                if (DEBUG) {
+                    LogManager.getLogger().d(
+                            LOG_TAG,
+                            "fling run(). CurrentX:" + mCurrentX + " CurrentY:"
+                                    + mCurrentY + " NewX:" + newX + " NewY:"
+                                    + newY);
+                }
+
+                mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
+                setImageViewMatrix(getDrawMatrix());
+
+                mCurrentX = newX;
+                mCurrentY = newY;
+
+                // Post On animation
+                Compat.postOnAnimation(imageView, this);
+            }
+        }
+    }
+}

+ 149 - 0
app/src/main/java/uk/co/senab/photoview/gestures/CupcakeGestureDetector.java

@@ -0,0 +1,149 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview.gestures;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import uk.co.senab.photoview.log.LogManager;
+
+public class CupcakeGestureDetector implements GestureDetector {
+
+    protected OnGestureListener mListener;
+    private static final String LOG_TAG = "CupcakeGestureDetector";
+    float mLastTouchX;
+    float mLastTouchY;
+    final float mTouchSlop;
+    final float mMinimumVelocity;
+
+    @Override
+    public void setOnGestureListener(OnGestureListener listener) {
+        this.mListener = listener;
+    }
+
+    public CupcakeGestureDetector(Context context) {
+        final ViewConfiguration configuration = ViewConfiguration
+                .get(context);
+        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+        mTouchSlop = configuration.getScaledTouchSlop();
+    }
+
+    private VelocityTracker mVelocityTracker;
+    private boolean mIsDragging;
+
+    float getActiveX(MotionEvent ev) {
+        return ev.getX();
+    }
+
+    float getActiveY(MotionEvent ev) {
+        return ev.getY();
+    }
+
+    @Override
+    public boolean isScaling() {
+        return false;
+    }
+
+    @Override
+    public boolean isDragging() {
+        return mIsDragging;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN: {
+                mVelocityTracker = VelocityTracker.obtain();
+                if (null != mVelocityTracker) {
+                    mVelocityTracker.addMovement(ev);
+                } else {
+                    LogManager.getLogger().i(LOG_TAG, "Velocity tracker is null");
+                }
+
+                mLastTouchX = getActiveX(ev);
+                mLastTouchY = getActiveY(ev);
+                mIsDragging = false;
+                break;
+            }
+
+            case MotionEvent.ACTION_MOVE: {
+                final float x = getActiveX(ev);
+                final float y = getActiveY(ev);
+                final float dx = x - mLastTouchX, dy = y - mLastTouchY;
+
+                if (!mIsDragging) {
+                    // Use Pythagoras to see if drag length is larger than
+                    // touch slop
+                    mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
+                }
+
+                if (mIsDragging) {
+                    mListener.onDrag(dx, dy);
+                    mLastTouchX = x;
+                    mLastTouchY = y;
+
+                    if (null != mVelocityTracker) {
+                        mVelocityTracker.addMovement(ev);
+                    }
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_CANCEL: {
+                // Recycle Velocity Tracker
+                if (null != mVelocityTracker) {
+                    mVelocityTracker.recycle();
+                    mVelocityTracker = null;
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_UP: {
+                if (mIsDragging) {
+                    if (null != mVelocityTracker) {
+                        mLastTouchX = getActiveX(ev);
+                        mLastTouchY = getActiveY(ev);
+
+                        // Compute velocity within the last 1000ms
+                        mVelocityTracker.addMovement(ev);
+                        mVelocityTracker.computeCurrentVelocity(1000);
+
+                        final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker
+                                .getYVelocity();
+
+                        // If the velocity is greater than minVelocity, call
+                        // listener
+                        if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) {
+                            mListener.onFling(mLastTouchX, mLastTouchY, -vX,
+                                    -vY);
+                        }
+                    }
+                }
+
+                // Recycle Velocity Tracker
+                if (null != mVelocityTracker) {
+                    mVelocityTracker.recycle();
+                    mVelocityTracker = null;
+                }
+                break;
+            }
+        }
+
+        return true;
+    }
+}

+ 91 - 0
app/src/main/java/uk/co/senab/photoview/gestures/EclairGestureDetector.java

@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ * <p/>
+ * 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
+ * <p/>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p/>
+ * 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 uk.co.senab.photoview.gestures;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.view.MotionEvent;
+
+import uk.co.senab.photoview.Compat;
+
+@TargetApi(5)
+public class EclairGestureDetector extends CupcakeGestureDetector {
+
+    private static final int INVALID_POINTER_ID = -1;
+    private int mActivePointerId = INVALID_POINTER_ID;
+    private int mActivePointerIndex = 0;
+
+    public EclairGestureDetector(Context context) {
+        super(context);
+    }
+
+    @Override
+    float getActiveX(MotionEvent ev) {
+        try {
+            return ev.getX(mActivePointerIndex);
+        } catch (Exception e) {
+            return ev.getX();
+        }
+    }
+
+    @Override
+    float getActiveY(MotionEvent ev) {
+        try {
+            return ev.getY(mActivePointerIndex);
+        } catch (Exception e) {
+            return ev.getY();
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        final int action = ev.getAction();
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN:
+                mActivePointerId = ev.getPointerId(0);
+                break;
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                mActivePointerId = INVALID_POINTER_ID;
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                // Ignore deprecation, ACTION_POINTER_ID_MASK and
+                // ACTION_POINTER_ID_SHIFT has same value and are deprecated
+                // You can have either deprecation or lint target api warning
+                final int pointerIndex = Compat.getPointerIndex(ev.getAction());
+                final int pointerId = ev.getPointerId(pointerIndex);
+                if (pointerId == mActivePointerId) {
+                    // This was our active pointer going up. Choose a new
+                    // active pointer and adjust accordingly.
+                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+                    mActivePointerId = ev.getPointerId(newPointerIndex);
+                    mLastTouchX = ev.getX(newPointerIndex);
+                    mLastTouchY = ev.getY(newPointerIndex);
+                }
+                break;
+        }
+
+        mActivePointerIndex = ev
+                .findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId
+                        : 0);
+        try {
+            return super.onTouchEvent(ev);
+        } catch (IllegalArgumentException e) {
+            // Fix for support lib bug, happening when onDestroy is
+            return true;
+        }
+    }
+}

+ 72 - 0
app/src/main/java/uk/co/senab/photoview/gestures/FroyoGestureDetector.java

@@ -0,0 +1,72 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ * <p/>
+ * 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
+ * <p/>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p/>
+ * 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 uk.co.senab.photoview.gestures;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+@TargetApi(8)
+public class FroyoGestureDetector extends EclairGestureDetector {
+
+    protected final ScaleGestureDetector mDetector;
+
+    public FroyoGestureDetector(Context context) {
+        super(context);
+        ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() {
+
+            @Override
+            public boolean onScale(ScaleGestureDetector detector) {
+                float scaleFactor = detector.getScaleFactor();
+
+                if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor))
+                    return false;
+
+                mListener.onScale(scaleFactor,
+                        detector.getFocusX(), detector.getFocusY());
+                return true;
+            }
+
+            @Override
+            public boolean onScaleBegin(ScaleGestureDetector detector) {
+                return true;
+            }
+
+            @Override
+            public void onScaleEnd(ScaleGestureDetector detector) {
+                // NO-OP
+            }
+        };
+        mDetector = new ScaleGestureDetector(context, mScaleListener);
+    }
+
+    @Override
+    public boolean isScaling() {
+        return mDetector.isInProgress();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        try {
+            mDetector.onTouchEvent(ev);
+            return super.onTouchEvent(ev);
+        } catch (IllegalArgumentException e) {
+            // Fix for support lib bug, happening when onDestroy is
+            return true;
+        }
+    }
+}

+ 30 - 0
app/src/main/java/uk/co/senab/photoview/gestures/GestureDetector.java

@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview.gestures;
+
+import android.view.MotionEvent;
+
+public interface GestureDetector {
+
+    boolean onTouchEvent(MotionEvent ev);
+
+    boolean isScaling();
+
+    boolean isDragging();
+
+    void setOnGestureListener(OnGestureListener listener);
+
+}

+ 27 - 0
app/src/main/java/uk/co/senab/photoview/gestures/OnGestureListener.java

@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview.gestures;
+
+public interface OnGestureListener {
+
+    void onDrag(float dx, float dy);
+
+    void onFling(float startX, float startY, float velocityX,
+                 float velocityY);
+
+    void onScale(float scaleFactor, float focusX, float focusY);
+
+}

+ 42 - 0
app/src/main/java/uk/co/senab/photoview/gestures/VersionedGestureDetector.java

@@ -0,0 +1,42 @@
+package uk.co.senab.photoview.gestures;
+
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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.
+ *******************************************************************************/
+
+import android.content.Context;
+import android.os.Build;
+
+public final class VersionedGestureDetector {
+
+    public static GestureDetector newInstance(Context context,
+                                              OnGestureListener listener) {
+        final int sdkVersion = Build.VERSION.SDK_INT;
+        GestureDetector detector;
+
+        if (sdkVersion < Build.VERSION_CODES.ECLAIR) {
+            detector = new CupcakeGestureDetector(context);
+        } else if (sdkVersion < Build.VERSION_CODES.FROYO) {
+            detector = new EclairGestureDetector(context);
+        } else {
+            detector = new FroyoGestureDetector(context);
+        }
+
+        detector.setOnGestureListener(listener);
+
+        return detector;
+    }
+
+}

+ 35 - 0
app/src/main/java/uk/co/senab/photoview/log/LogManager.java

@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview.log;
+
+import android.util.Log;
+
+/**
+ * class that holds the {@link Logger} for this library, defaults to {@link LoggerDefault} to send logs to android {@link Log}
+ */
+public final class LogManager {
+
+    private static Logger logger = new LoggerDefault();
+
+    public static void setLogger(Logger newLogger) {
+        logger = newLogger;
+    }
+
+    public static Logger getLogger() {
+        return logger;
+    }
+
+}

+ 42 - 0
app/src/main/java/uk/co/senab/photoview/log/Logger.java

@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview.log;
+
+/**
+ * interface for a logger class to replace the static calls to {@link android.util.Log}
+ */
+public interface Logger {
+
+    int v(String tag, String msg);
+
+    int v(String tag, String msg, Throwable tr);
+
+    int d(String tag, String msg);
+
+    int d(String tag, String msg, Throwable tr);
+
+    int i(String tag, String msg);
+
+    int i(String tag, String msg, Throwable tr);
+
+    int w(String tag, String msg);
+
+    int w(String tag, String msg, Throwable tr);
+
+    int e(String tag, String msg);
+
+    int e(String tag, String msg, Throwable tr);
+}

+ 76 - 0
app/src/main/java/uk/co/senab/photoview/log/LoggerDefault.java

@@ -0,0 +1,76 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview.log;
+
+import android.util.Log;
+
+/**
+ * Helper class to redirect {@link LogManager#logger} to {@link Log}
+ */
+public class LoggerDefault implements Logger {
+
+    @Override
+    public int v(String tag, String msg) {
+        return Log.v(tag, msg);
+    }
+
+    @Override
+    public int v(String tag, String msg, Throwable tr) {
+        return Log.v(tag, msg, tr);
+    }
+
+    @Override
+    public int d(String tag, String msg) {
+        return Log.d(tag, msg);
+    }
+
+    @Override
+    public int d(String tag, String msg, Throwable tr) {
+        return Log.d(tag, msg, tr);
+    }
+
+    @Override
+    public int i(String tag, String msg) {
+        return Log.i(tag, msg);
+    }
+
+    @Override
+    public int i(String tag, String msg, Throwable tr) {
+        return Log.i(tag, msg, tr);
+    }
+
+    @Override
+    public int w(String tag, String msg) {
+        return Log.w(tag, msg);
+    }
+
+    @Override
+    public int w(String tag, String msg, Throwable tr) {
+        return Log.w(tag, msg, tr);
+    }
+
+    @Override
+    public int e(String tag, String msg) {
+        return Log.e(tag, msg);
+    }
+
+    @Override
+    public int e(String tag, String msg, Throwable tr) {
+        return Log.e(tag, msg, tr);
+    }
+
+
+}

+ 61 - 0
app/src/main/java/uk/co/senab/photoview/scrollerproxy/GingerScroller.java

@@ -0,0 +1,61 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview.scrollerproxy;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.widget.OverScroller;
+
+@TargetApi(9)
+public class GingerScroller extends ScrollerProxy {
+
+    protected final OverScroller mScroller;
+
+    public GingerScroller(Context context) {
+        mScroller = new OverScroller(context);
+    }
+
+    @Override
+    public boolean computeScrollOffset() {
+        return mScroller.computeScrollOffset();
+    }
+
+    @Override
+    public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY,
+                      int overX, int overY) {
+        mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY);
+    }
+
+    @Override
+    public void forceFinished(boolean finished) {
+        mScroller.forceFinished(finished);
+    }
+
+    @Override
+    public boolean isFinished() {
+        return mScroller.isFinished();
+    }
+
+    @Override
+    public int getCurrX() {
+        return mScroller.getCurrX();
+    }
+
+    @Override
+    public int getCurrY() {
+        return mScroller.getCurrY();
+    }
+}

+ 33 - 0
app/src/main/java/uk/co/senab/photoview/scrollerproxy/IcsScroller.java

@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview.scrollerproxy;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+
+@TargetApi(14)
+public class IcsScroller extends GingerScroller {
+
+    public IcsScroller(Context context) {
+        super(context);
+    }
+
+    @Override
+    public boolean computeScrollOffset() {
+        return mScroller.computeScrollOffset();
+    }
+
+}

+ 58 - 0
app/src/main/java/uk/co/senab/photoview/scrollerproxy/PreGingerScroller.java

@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview.scrollerproxy;
+
+import android.content.Context;
+import android.widget.Scroller;
+
+public class PreGingerScroller extends ScrollerProxy {
+
+    private final Scroller mScroller;
+
+    public PreGingerScroller(Context context) {
+        mScroller = new Scroller(context);
+    }
+
+    @Override
+    public boolean computeScrollOffset() {
+        return mScroller.computeScrollOffset();
+    }
+
+    @Override
+    public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY,
+                      int overX, int overY) {
+        mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
+    }
+
+    @Override
+    public void forceFinished(boolean finished) {
+        mScroller.forceFinished(finished);
+    }
+
+    public boolean isFinished() {
+        return mScroller.isFinished();
+    }
+
+    @Override
+    public int getCurrX() {
+        return mScroller.getCurrX();
+    }
+
+    @Override
+    public int getCurrY() {
+        return mScroller.getCurrY();
+    }
+}

+ 48 - 0
app/src/main/java/uk/co/senab/photoview/scrollerproxy/ScrollerProxy.java

@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright 2011, 2012 Chris Banes.
+ *
+ * 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 uk.co.senab.photoview.scrollerproxy;
+
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+public abstract class ScrollerProxy {
+
+    public static ScrollerProxy getScroller(Context context) {
+        if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) {
+            return new PreGingerScroller(context);
+        } else if (VERSION.SDK_INT < VERSION_CODES.ICE_CREAM_SANDWICH) {
+            return new GingerScroller(context);
+        } else {
+            return new IcsScroller(context);
+        }
+    }
+
+    public abstract boolean computeScrollOffset();
+
+    public abstract void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY,
+                               int maxY, int overX, int overY);
+
+    public abstract void forceFinished(boolean finished);
+
+    public abstract boolean isFinished();
+
+    public abstract int getCurrX();
+
+    public abstract int getCurrY();
+
+
+}

BIN
app/src/main/res/drawable-hdpi/ic_info_black_24dp.png


BIN
app/src/main/res/drawable-hdpi/ic_notifications_black_24dp.png


BIN
app/src/main/res/drawable-hdpi/ic_sync_black_24dp.png


BIN
app/src/main/res/drawable-mdpi/ic_info_black_24dp.png


BIN
app/src/main/res/drawable-mdpi/ic_notifications_black_24dp.png


BIN
app/src/main/res/drawable-mdpi/ic_sync_black_24dp.png


+ 9 - 0
app/src/main/res/drawable-v21/ic_info_black_24dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm1,15h-2v-6h2v6zm0,-8h-2V7h2v2z" />
+</vector>

+ 9 - 0
app/src/main/res/drawable-v21/ic_notifications_black_24dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M11.5,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zm6.5,-6v-5.5c0,-3.07 -2.13,-5.64 -5,-6.32V3.5c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,2.67 10,3.5v0.68c-2.87,0.68 -5,3.25 -5,6.32V16l-2,2v1h17v-1l-2,-2z" />
+</vector>

+ 9 - 0
app/src/main/res/drawable-v21/ic_sync_black_24dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01,-.25 1.97,-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0,-4.42,-3.58,-8,-8,-8zm0 14c-3.31 0,-6,-2.69,-6,-6 0,-1.01.25,-1.97.7,-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4,-4,-4,-4v3z" />
+</vector>

BIN
app/src/main/res/drawable-xhdpi/ic_info_black_24dp.png


BIN
app/src/main/res/drawable-xhdpi/ic_notifications_black_24dp.png


BIN
app/src/main/res/drawable-xhdpi/ic_sync_black_24dp.png


BIN
app/src/main/res/drawable-xxhdpi/ic_info_black_24dp.png


BIN
app/src/main/res/drawable-xxhdpi/ic_notifications_black_24dp.png


BIN
app/src/main/res/drawable-xxhdpi/ic_sync_black_24dp.png


BIN
app/src/main/res/drawable-xxxhdpi/ic_info_black_24dp.png


BIN
app/src/main/res/drawable-xxxhdpi/ic_notifications_black_24dp.png


BIN
app/src/main/res/drawable-xxxhdpi/ic_sync_black_24dp.png


BIN
app/src/main/res/drawable/carte.png


BIN
app/src/main/res/drawable/game_medium_map.png


BIN
app/src/main/res/drawable/inventaire.png


BIN
app/src/main/res/drawable/options.png


BIN
app/src/main/res/drawable/scanner.png


+ 137 - 21
app/src/main/res/layout/activity_menu.xml

@@ -1,34 +1,150 @@
 <?xml version="1.0" encoding="utf-8"?>
-<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:fitsSystemWindows="true"
-    tools:context=".MenuActivity">
+    tools:context=".MenuActivity"
+    android:orientation="vertical"
+    android:background="#000000">
 
-    <android.support.design.widget.AppBarLayout
+
+    <FrameLayout
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:theme="@style/AppTheme.AppBarOverlay">
+        android:layout_height="match_parent">
 
-        <android.support.v7.widget.Toolbar
-            android:id="@+id/toolbar"
+        <LinearLayout
+            android:orientation="vertical"
             android:layout_width="match_parent"
-            android:layout_height="?attr/actionBarSize"
-            android:background="?attr/colorPrimary"
-            app:popupTheme="@style/AppTheme.PopupOverlay" />
+            android:layout_height="match_parent"
+            android:showDividers="end"
+            android:measureWithLargestChild="true"
+            android:gravity="center_vertical|center_horizontal"
+            android:weightSum="1">
+
+            <LinearLayout
+                android:orientation="horizontal"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:weightSum="1"
+                android:layout_marginLeft="40dp"
+                android:layout_marginRight="40dp"
+                android:id="@+id/ll_1">
+
+                <ImageButton
+                    android:layout_width="103dp"
+                    android:layout_height="130dp"
+                    android:id="@+id/ib_scanner"
+                    android:src="@drawable/scanner"
+                    android:scaleType="fitCenter"
+                    android:background="#000000"
+                    android:onClick="onScannerClick" />
+
+                <Button
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:text="Scanner"
+                    android:id="@+id/b_scanner"
+                    android:textColor="#33b5e5"
+                    android:textSize="25dp"
+                    android:textStyle="bold"
+                    android:background="#000000"
+                    android:onClick="onScannerClick" />
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:orientation="horizontal"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="40dp"
+                android:layout_marginTop="40dp"
+                android:layout_marginRight="40dp"
+                android:id="@+id/ll_2">
+
+                <ImageButton
+                    android:layout_width="118dp"
+                    android:layout_height="130dp"
+                    android:id="@+id/ib_inventaire"
+                    android:src="@drawable/inventaire"
+                    android:scaleType="fitCenter"
+                    android:background="#000000"
+                    android:onClick="onInventaireClick" />
+
+                <Button
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:text="Inventaire"
+                    android:id="@+id/b_inventaire"
+                    android:textColor="#33b5e5"
+                    android:textStyle="bold"
+                    android:textSize="25dp"
+                    android:background="#000000"
+                    android:onClick="onInventaireClick" />
+            </LinearLayout>
+
+            <LinearLayout
+                android:orientation="horizontal"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="40dp"
+                android:layout_marginTop="40dp"
+                android:layout_marginRight="40dp"
+                android:id="@+id/ll_3">
+
+                <ImageButton
+                    android:layout_width="100dp"
+                    android:layout_height="130dp"
+                    android:id="@+id/ib_carte"
+                    android:src="@drawable/carte"
+                    android:scaleType="fitCenter"
+                    android:background="#000000"
+                    android:onClick="onCarteClick" />
+
+                <Button
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:text="Carte"
+                    android:id="@+id/b_carte"
+                    android:textStyle="bold"
+                    android:textSize="25dp"
+                    android:textColor="#33b5e5"
+                    android:background="#000000"
+                    android:onClick="onCarteClick" />
+            </LinearLayout>
+
+            <LinearLayout
+                android:orientation="horizontal"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="40dp"
+                android:layout_marginTop="40dp"
+                android:layout_marginRight="40dp"
+                android:id="@+id/ll_4">
 
-    </android.support.design.widget.AppBarLayout>
+                <ImageButton
+                    android:layout_width="110dp"
+                    android:layout_height="130dp"
+                    android:id="@+id/ib_options"
+                    android:scaleType="fitCenter"
+                    android:src="@drawable/options"
+                    android:background="#000000"
+                    android:onClick="onOptionsClick" />
 
-    <include layout="@layout/content_menu" />
+                <Button
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:text="Options"
+                    android:id="@+id/b_option"
+                    android:textColor="#33b5e5"
+                    android:textSize="25dp"
+                    android:textStyle="bold"
+                    android:background="#000000"
+                    android:onClick="onOptionsClick" />
+            </LinearLayout>
 
-    <android.support.design.widget.FloatingActionButton
-        android:id="@+id/fab"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="bottom|end"
-        android:layout_margin="@dimen/fab_margin"
-        android:src="@android:drawable/ic_dialog_email" />
+        </LinearLayout>
 
-</android.support.design.widget.CoordinatorLayout>
+    </FrameLayout>
+</LinearLayout>

+ 1 - 1
app/src/main/res/layout/activity_viewer.xml

@@ -13,7 +13,7 @@
          android:fitsSystemWindows. -->
 
     <Button
-        android:layout_width="wrap_content"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="Informations"
         android:id="@+id/button_info"

+ 59 - 0
app/src/main/res/values/strings.xml

@@ -11,4 +11,63 @@
 
     <string name="title_activity_resource_list">ResourceListActivity</string>
     <string name="title_activity_menu">MenuActivity</string>
+    <string name="title_activity_settings">Settings</string>
+
+    <!-- Strings related to Settings -->
+
+    <!-- Example General settings -->
+    <string name="pref_header_general">General</string>
+
+    <string name="pref_title_social_recommendations">Enable social recommendations</string>
+    <string name="pref_description_social_recommendations">Recommendations for people to contact
+        based on your message history
+    </string>
+
+    <string name="pref_title_display_name">Display name</string>
+    <string name="pref_default_display_name">John Smith</string>
+
+    <string name="pref_title_add_friends_to_messages">Add friends to messages</string>
+    <string-array name="pref_example_list_titles">
+        <item>Always</item>
+        <item>When possible</item>
+        <item>Never</item>
+    </string-array>
+    <string-array name="pref_example_list_values">
+        <item>1</item>
+        <item>0</item>
+        <item>-1</item>
+    </string-array>
+
+    <!-- Example settings for Data & Sync -->
+    <string name="pref_header_data_sync">Data &amp; sync</string>
+
+    <string name="pref_title_sync_frequency">Sync frequency</string>
+    <string-array name="pref_sync_frequency_titles">
+        <item>15 minutes</item>
+        <item>30 minutes</item>
+        <item>1 hour</item>
+        <item>3 hours</item>
+        <item>6 hours</item>
+        <item>Never</item>
+    </string-array>
+    <string-array name="pref_sync_frequency_values">
+        <item>15</item>
+        <item>30</item>
+        <item>60</item>
+        <item>180</item>
+        <item>360</item>
+        <item>-1</item>
+    </string-array>
+
+    <string name="pref_title_system_sync_settings">System sync settings</string>
+
+    <!-- Example settings for Notifications -->
+    <string name="pref_header_notifications">Notifications</string>
+
+    <string name="pref_title_new_message_notifications">New message notifications</string>
+
+    <string name="pref_title_ringtone">Ringtone</string>
+    <string name="pref_ringtone_silent">Silent</string>
+
+    <string name="pref_title_vibrate">Vibrate</string>
 </resources>

+ 21 - 0
app/src/main/res/xml/pref_data_sync.xml

@@ -0,0 +1,21 @@
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <!-- NOTE: Hide buttons to simplify the UI. Users can touch outside the dialog to
+         dismiss it. -->
+    <!-- NOTE: ListPreference's summary should be set to its value by the activity code. -->
+    <ListPreference
+        android:defaultValue="180"
+        android:entries="@array/pref_sync_frequency_titles"
+        android:entryValues="@array/pref_sync_frequency_values"
+        android:key="sync_frequency"
+        android:negativeButtonText="@null"
+        android:positiveButtonText="@null"
+        android:title="@string/pref_title_sync_frequency" />
+
+    <!-- This preference simply launches an intent when selected. Use this UI sparingly, per
+         design guidelines. -->
+    <Preference android:title="@string/pref_title_system_sync_settings">
+        <intent android:action="android.settings.SYNC_SETTINGS" />
+    </Preference>
+
+</PreferenceScreen>

+ 33 - 0
app/src/main/res/xml/pref_general.xml

@@ -0,0 +1,33 @@
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <SwitchPreference
+        android:defaultValue="true"
+        android:key="example_switch"
+        android:summary="@string/pref_description_social_recommendations"
+        android:title="@string/pref_title_social_recommendations" />
+
+    <!-- NOTE: EditTextPreference accepts EditText attributes. -->
+    <!-- NOTE: EditTextPreference's summary should be set to its value by the activity code. -->
+    <EditTextPreference
+        android:capitalize="words"
+        android:defaultValue="@string/pref_default_display_name"
+        android:inputType="textCapWords"
+        android:key="example_text"
+        android:maxLines="1"
+        android:selectAllOnFocus="true"
+        android:singleLine="true"
+        android:title="@string/pref_title_display_name" />
+
+    <!-- NOTE: Hide buttons to simplify the UI. Users can touch outside the dialog to
+         dismiss it. -->
+    <!-- NOTE: ListPreference's summary should be set to its value by the activity code. -->
+    <ListPreference
+        android:defaultValue="-1"
+        android:entries="@array/pref_example_list_titles"
+        android:entryValues="@array/pref_example_list_values"
+        android:key="example_list"
+        android:negativeButtonText="@null"
+        android:positiveButtonText="@null"
+        android:title="@string/pref_title_add_friends_to_messages" />
+
+</PreferenceScreen>

+ 20 - 0
app/src/main/res/xml/pref_headers.xml

@@ -0,0 +1,20 @@
+<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <!-- These settings headers are only used on tablets. -->
+
+    <header
+        android:fragment="app.brest.testmin3d.SettingsActivity$GeneralPreferenceFragment"
+        android:icon="@drawable/ic_info_black_24dp"
+        android:title="@string/pref_header_general" />
+
+    <header
+        android:fragment="app.brest.testmin3d.SettingsActivity$NotificationPreferenceFragment"
+        android:icon="@drawable/ic_notifications_black_24dp"
+        android:title="@string/pref_header_notifications" />
+
+    <header
+        android:fragment="app.brest.testmin3d.SettingsActivity$DataSyncPreferenceFragment"
+        android:icon="@drawable/ic_sync_black_24dp"
+        android:title="@string/pref_header_data_sync" />
+
+</preference-headers>

+ 27 - 0
app/src/main/res/xml/pref_notification.xml

@@ -0,0 +1,27 @@
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <!-- A 'parent' preference, which enables/disables child preferences (below)
+         when checked/unchecked. -->
+    <SwitchPreference
+        android:defaultValue="true"
+        android:key="notifications_new_message"
+        android:title="@string/pref_title_new_message_notifications" />
+
+    <!-- Allows the user to choose a ringtone in the 'notification' category. -->
+    <!-- NOTE: This preference will be enabled only when the checkbox above is checked. -->
+    <!-- NOTE: RingtonePreference's summary should be set to its value by the activity code. -->
+    <RingtonePreference
+        android:defaultValue="content://settings/system/notification_sound"
+        android:dependency="notifications_new_message"
+        android:key="notifications_new_message_ringtone"
+        android:ringtoneType="notification"
+        android:title="@string/pref_title_ringtone" />
+
+    <!-- NOTE: This preference will be enabled only when the checkbox above is checked. -->
+    <SwitchPreference
+        android:defaultValue="true"
+        android:dependency="notifications_new_message"
+        android:key="notifications_new_message_vibrate"
+        android:title="@string/pref_title_vibrate" />
+
+</PreferenceScreen>