Commit 56679c1c authored by Sajal Narang's avatar Sajal Narang Committed by GitHub

Merge pull request #149 from pulsejet/map

InstiMap!
parents 86254a47 f2966a19
......@@ -14,6 +14,7 @@ import app.insti.data.Notification;
import app.insti.data.PlacementBlogPost;
import app.insti.data.TrainingBlogPost;
import app.insti.data.User;
import app.insti.data.Venue;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
......@@ -38,6 +39,9 @@ public interface RetrofitInterface {
@GET("events")
Call<NewsFeedResponse> getNewsFeed(@Header("Cookie") String sessionId);
@GET("locations")
Call<List<Venue>> getAllVenues();
@GET("events")
Call<NewsFeedResponse> getEventsBetweenDates(@Header("Cookie") String sessionId, @Query("start") String start, @Query("end") String end);
......
......@@ -145,7 +145,7 @@ public class CalendarFragment extends BaseFragment {
private void showEventsForDate(Date date) {
/* Skip if we're already destroyed */
if (getActivity() == null) return;
if (getActivity() == null || getView() == null) return;
final List<Event> filteredEvents = new ArrayList<Event>();
for (Event event : events) {
......
......@@ -128,7 +128,7 @@ public class FeedFragment extends BaseFragment {
private void displayEvents(final List<Event> events) {
/* Skip if we're already destroyed */
if (getActivity() == null) return;
if (getActivity() == null || getView() == null) return;
final FeedAdapter feedAdapter = new FeedAdapter(events, new ItemClickListener() {
@Override
......
package app.insti.fragment;
import android.Manifest;
import android.app.AlertDialog;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.location.Location;
import android.location.LocationManager;
import android.os.Build;
import android.graphics.PointF;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.widget.Toolbar;
import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.TextPaint;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.StyleSpan;
import android.text.util.Linkify;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ExpandableListView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.location.LocationSettingsResult;
import com.google.android.gms.location.LocationSettingsStates;
import com.google.android.gms.location.LocationSettingsStatusCodes;
import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.mrane.campusmap.ExpandableListAdapter;
import com.mrane.campusmap.FuzzySearchAdapter;
import com.mrane.campusmap.IndexFragment;
import com.mrane.campusmap.ListFragment;
import com.mrane.campusmap.SettingsManager;
import com.mrane.data.Building;
import com.mrane.data.Locations;
import com.mrane.data.Room;
import com.mrane.navigation.CardSlideListener;
import com.mrane.navigation.SlidingUpPanelLayout;
import com.mrane.zoomview.CampusMapView;
import com.mrane.zoomview.SubsamplingScaleImageView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import app.insti.Constants;
import app.insti.R;
import app.insti.api.RetrofitInterface;
import app.insti.api.ServiceGenerator;
import app.insti.data.Venue;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static android.content.Context.LOCATION_SERVICE;
public class MapFragment extends Fragment implements TextWatcher,
TextView.OnEditorActionListener, AdapterView.OnItemClickListener, View.OnFocusChangeListener,
View.OnTouchListener, ExpandableListView.OnChildClickListener {
private static MapFragment mainactivity;
private SettingsManager settingsManager;
private FuzzySearchAdapter adapter;
private ExpandableListAdapter expAdapter;
private FragmentManager fragmentManager;
private ListFragment listFragment;
private IndexFragment indexFragment;
private Fragment fragment;
public LinearLayout newSmallCard;
public ImageView placeColor;
private RelativeLayout fragmentContainer;
private View actionBarView;
public TextView placeNameTextView;
public TextView placeSubHeadTextView;
public EditText editText;
public HashMap<String, com.mrane.data.Marker> data;
private List<com.mrane.data.Marker> markerlist;
public FragmentTransaction transaction;
public CampusMapView campusMapView;
public ImageButton removeIcon;
public ImageButton indexIcon;
public ImageButton mapIcon;
public ImageButton addMarkerIcon;
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
private SlidingUpPanelLayout slidingLayout;
private CardSlideListener cardSlideListener;
private boolean noFragments = true;
private boolean editTextFocused = false;
private final String firstStackTag = "FIRST_TAG";
private final int MSG_ANIMATE = 1;
private final int MSG_PLAY_SOUND = 2;
private final int MSG_DISPLAY_MAP = 3;
private final long DELAY_ANIMATE = 150;
private final long DELAY_INIT_LAYOUT = 250;
private Toast toast;
private String message = "Sorry, no such place in our data.";
public static final PointF MAP_CENTER = new PointF(2971f, 1744f);
public static final long DURATION_INIT_MAP_ANIM = 500;
public static final String FONT_SEMIBOLD = "rigascreen_bold.ttf";
public static final String FONT_REGULAR = "rigascreen_regular.ttf";
public static final int SOUND_ID_RESULT = 0;
public static final int SOUND_ID_ADD = 1;
public static final int SOUND_ID_REMOVE = 2;
public SoundPool soundPool;
public int[] soundPoolIds;
public class MapFragment extends BaseFragment implements OnMapReadyCallback, LocationListener,
GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_ANIMATE:
showResultOnMap((String) msg.obj);
break;
case MSG_PLAY_SOUND:
playAnimSound(msg.arg1);
break;
case MSG_DISPLAY_MAP:
displayMap();
break;
}
}
};
public MapFragment() {
// Required empty public constructor
}
SupportMapFragment gMapFragment;
GoogleMap googleMap;
GoogleApiClient mGoogleApiClient;
Location mLastLocation;
Marker mCurrLocationMarker;
LocationRequest mLocationRequest;
private FloatingActionButton locationButton;
private Location currentLocation;
@Override
public void onCreate(Bundle savedInstanceState) {
mainactivity = this;
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_map, container, false);
gMapFragment = (SupportMapFragment) this.getChildFragmentManager().findFragmentById(R.id.viewMap);
gMapFragment.getMapAsync(this);
return view;
return inflater.inflate(R.layout.fragment_map, container, false);
}
@Override
public void onStart() {
super.onStart();
/* Set title */
Toolbar toolbar = getActivity().findViewById(R.id.toolbar);
toolbar.setTitle("Map");
toolbar.setTitle("InstiMap");
RetrofitInterface retrofitInterface = ServiceGenerator.createService(RetrofitInterface.class);
retrofitInterface.getAllVenues().enqueue(new Callback<List<Venue>>() {
@Override
public void onResponse(Call<List<Venue>> call, Response<List<Venue>> response) {
if (response.isSuccessful()) {
Locations mLocations = new Locations(response.body());
data = mLocations.data;
markerlist = new ArrayList<com.mrane.data.Marker>(data.values());
setUpDrawer();
setupMap();
}
}
@Override
public void onFailure(Call<List<Venue>> call, Throwable t) { }
});
}
private void setupMap() {
if (getView() == null) {
return;
}
newSmallCard = (LinearLayout) getActivity().findViewById(R.id.new_small_card);
slidingLayout = (SlidingUpPanelLayout) getActivity().findViewById(R.id.sliding_layout);
placeNameTextView = (TextView) getActivity().findViewById(R.id.place_name);
placeColor = (ImageView) getActivity().findViewById(R.id.place_color);
placeSubHeadTextView = (TextView) getActivity().findViewById(R.id.place_sub_head);
cardSlideListener = new CardSlideListener(this);
slidingLayout.setPanelSlideListener(cardSlideListener);
slidingLayout.post(setAnchor());
locationButton = (FloatingActionButton) getActivity().findViewById(R.id.location_button);
locationButton.setImageResource(R.drawable.ic_my_location_black_24dp);
locationButton.getDrawable().setColorFilter(ContextCompat.getColor(getContext(), R.color.colorPrimaryDark), PorterDuff.Mode.SRC_IN);
initShowDefault();
initImageUri();
locationButton.setOnClickListener(new View.OnClickListener() {
fragmentContainer = (RelativeLayout) getActivity().findViewById(R.id.fragment_container);
adapter = new FuzzySearchAdapter(getContext(), markerlist);
editText = (EditText)getView().findViewById(R.id.search);
editText.addTextChangedListener(this);
editText.setOnEditorActionListener(this);
editText.setOnFocusChangeListener(this);
settingsManager = new SettingsManager(getContext());
campusMapView = (CampusMapView) getActivity().findViewById(R.id.campusMapView);
campusMapView.setImageAsset("map.jpg");
campusMapView.setSettingsManager(settingsManager);
campusMapView.setData(data);
removeIcon = (ImageButton) getActivity().findViewById(R.id.remove_icon);
removeIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
removeClick(v);
}
});
indexIcon = (ImageButton) getActivity().findViewById(R.id.index_icon);
indexIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
indexClick(v);
}
});
mapIcon = (ImageButton) getActivity().findViewById(R.id.map_icon);
mapIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.v("MapFragment", "Location button pressed");
try {
LocationManager lm = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);
boolean gps_enabled = false;
boolean network_enabled = false;
if (ActivityCompat.checkSelfPermission(getActivity(), android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(getContext(), android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, Constants.MY_PERMISSIONS_REQUEST_LOCATION);
return;
}
try {
gps_enabled = lm.isProviderEnabled(LocationManager.GPS_PROVIDER);
} catch (Exception ex) {
}
try {
network_enabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
} catch (Exception ex) {
}
if (!gps_enabled && !network_enabled) {
LocationRequest locationRequest = LocationRequest.create();
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
locationRequest.setInterval(30 * 1000);
locationRequest.setFastestInterval(5 * 1000);
LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder()
.addLocationRequest(locationRequest);
//**************************
builder.setAlwaysShow(true); //this is the key ingredient
//**************************
PendingResult<LocationSettingsResult> result =
LocationServices.SettingsApi.checkLocationSettings(mGoogleApiClient, builder.build());
result.setResultCallback(new ResultCallback<LocationSettingsResult>() {
@Override
public void onResult(LocationSettingsResult result) {
final Status status = result.getStatus();
final LocationSettingsStates state = result.getLocationSettingsStates();
switch (status.getStatusCode()) {
case LocationSettingsStatusCodes.SUCCESS:
// All location settings are satisfied. The client can initialize location
// requests here.
break;
case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
// Location settings are not satisfied. But could be fixed by showing the user
// a dialog.
try {
// Show the dialog by calling startResolutionForResult(),
// and check the result in onActivityResult().
status.startResolutionForResult(
getActivity(), 1000);
} catch (IntentSender.SendIntentException e) {
// Ignore the error.
}
break;
case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
// Location settings are not satisfied. However, we have no way to fix the
// settings so we won't show the dialog.
break;
}
}
});
}
currentLocation = getLastKnownLocation();
if (currentLocation != null) {
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude()), 17);
googleMap.animateCamera(cameraUpdate);
locationButton.getDrawable().setColorFilter(ContextCompat.getColor(getContext(), R.color.colorPrimary), PorterDuff.Mode.SRC_IN);
}
} catch (Exception e) {
checkLocationPermission();
Toast.makeText(getContext(), "Please turn on Location from the Settings", Toast.LENGTH_SHORT).show();
mapClick(v);
}
});
addMarkerIcon = (ImageButton) getActivity().findViewById(R.id.add_marker_icon);
}
fragmentManager = getChildFragmentManager();
listFragment = new ListFragment();
indexFragment = new IndexFragment();
adapter.setSettingsManager(settingsManager);
initSoundPool();
setFonts();
getActivity().findViewById(R.id.add_marker_icon).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addMarkerClick(v);
}
});
getActivity().findViewById(R.id.loadingPanel).setVisibility(View.GONE);
}
private Location getLastKnownLocation() {
//Create a location manager object instance
LocationManager mLocationManager = (LocationManager) getContext().getSystemService(LOCATION_SERVICE);
//Get all the different providers which can give current location info
List<String> providers = mLocationManager.getProviders(true);
//Initialising the location to be null
Location bestLocation = null;
//Creating a for loop to go through all the location providers and get the location
for (String provider : providers) {
private void setUpDrawer() {
Toolbar mToolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
actionBarView = getActivity().findViewById(R.id.toolbar);
mDrawerLayout = (DrawerLayout) getActivity().findViewById(R.id.drawer_layout);
mDrawerToggle = new ActionBarDrawerToggle(getActivity(),
mDrawerLayout,
mToolbar,
R.string.drawer_open,
R.string.drawer_close) {
//if permission is not granted, then get the permission
TextView settingsTitle = (TextView) getActivity().findViewById(R.id.settings_title);
if (ContextCompat.checkSelfPermission(getContext(), android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
/** Called when a drawer has settled in a completely closed state. */
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
settingsTitle.setVisibility(View.GONE);
editText.setVisibility(View.VISIBLE);
setCorrectIcons();
}
ActivityCompat.requestPermissions(getActivity(), new String[]{android.Manifest.permission.ACCESS_COARSE_LOCATION},
Constants.MY_PERMISSIONS_REQUEST_LOCATION);
/** Called when a drawer has settled in a completely open state. */
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
editText.setVisibility(View.GONE);
indexIcon.setVisibility(View.GONE);
mapIcon.setVisibility(View.GONE);
removeIcon.setVisibility(View.GONE);
settingsTitle.setVisibility(View.VISIBLE);
}
//Get the last known location from the data provider
Location l = mLocationManager.getLastKnownLocation(provider);
//If the last know location provided by the data provider is null then ignore the provider and move to the next one.
if (l == null) {
continue;
};
mDrawerLayout.setDrawerListener(mDrawerToggle);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
// TODO Auto-generated method stub
super.onConfigurationChanged(newConfig);
mDrawerToggle.onConfigurationChanged(newConfig);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// TODO Auto-generated method stub
if (mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
return super.onOptionsItemSelected(item);
}
private void initShowDefault() {
String[] keys = { "Convocation Hall", "Hostel 13 House of Titans",
"Hostel 15", "Main Gate no. 2",
"Market Gate, Y point Gate no. 3", "Lake Side Gate no. 1", };
for (String key : keys) {
if (data.containsKey(key)) {
data.get(key).setShowDefault(true);
} else {
Log.d("null point", "key not found (initShowDefault): " + key);
}
}
}
if (bestLocation == null || l.getAccuracy() < bestLocation.getAccuracy()) {
bestLocation = l;
private void initImageUri() {
String[] keys = { "Convocation Hall", "Guest House/ Jalvihar",
"Guest House/ Vanvihar", "Gulmohar Restaurant", "Hostel 14",
"Industrial Design Centre", "Main Building",
"Nestle Cafe (Coffee Shack)", "School of Management",
"Victor Menezes Convention Centre" };
String[] uri = { "convo_hall", "jalvihar", "vanvihar", "gulmohar",
"h14", "idc", "mainbuilding", "nescafestall", "som", "vmcc" };
for (int i = 0; i < keys.length; i++) {
if (data.containsKey(keys[i])) {
data.get(keys[i]).setImageUri(uri[i]);
} else {
Log.d("null point", "check " + keys[i]);
}
}
return bestLocation;
}
private void setFonts() {
Typeface regular = Typeface.createFromAsset(getActivity().getAssets(), FONT_REGULAR);
placeNameTextView.setTypeface(regular, Typeface.BOLD);
placeSubHeadTextView.setTypeface(regular);
editText.setTypeface(regular);
TextView settingsTitle = (TextView) getActivity()
.findViewById(R.id.settings_title);
settingsTitle.setTypeface(regular);
}
private Runnable setAnchor() {
Runnable runnable = new Runnable() {
@Override
public void run() {
int totalHeight = slidingLayout.getHeight();
int expandedCardHeight = getResources().getDimensionPixelSize(
R.dimen.expanded_card_height);
float anchorPoint = 0.5f;
slidingLayout.setAnchorPoint(anchorPoint);
Log.d("testing", "Anchor point = " + anchorPoint);
}
};
return runnable;
}
private void initSoundPool() {
soundPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 100);
soundPoolIds = new int[3];
soundPoolIds[SOUND_ID_RESULT] = soundPool.load(getContext(),
R.raw.result_marker, 1);
soundPoolIds[SOUND_ID_ADD] = soundPool.load(getContext(), R.raw.add_marker, 2);
soundPoolIds[SOUND_ID_REMOVE] = soundPool.load(getContext(),
R.raw.remove_marker, 3);
}
@Override
public void afterTextChanged(Editable arg0) {
String text = editText.getText().toString()
.toLowerCase(Locale.getDefault());
adapter.filter(refineText(text));
}
private String refineText(String text) {
String refinedText = text.replaceAll(Pattern.quote("("), "@")
.replaceAll(Pattern.quote(")"), "@")
.replaceAll(Pattern.quote("."), "@")
.replaceAll(Pattern.quote("+"), "@")
.replaceAll(Pattern.quote("{"), "@")
.replaceAll(Pattern.quote("?"), "@")
.replaceAll(Pattern.quote("\\"), "@")
.replaceAll(Pattern.quote("["), "@")
.replaceAll(Pattern.quote("^"), "@")
.replaceAll(Pattern.quote("$"), "@");
return refinedText;
}
@Override
public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
int arg3) {
// TODO Auto-generated method stub
}
@Override
public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
this.setCorrectIcons();
}
@Override
public void onMapReady(GoogleMap gMap) {
googleMap = gMap;
if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
checkLocationPermission();
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if ((actionId == EditorInfo.IME_ACTION_SEARCH)
|| (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
onItemClick(null, v, 0, 0);
}
return true;
}
public static MapFragment getMainActivity() {
return mainactivity;
}
private void putFragment(Fragment tempFragment) {
this.dismissCard();
transaction = fragmentManager.beginTransaction();
fragment = tempFragment;
if (noFragments) {
transaction.add(R.id.fragment_container, tempFragment);
transaction.addToBackStack(firstStackTag);
transaction.commit();
} else {
googleMap.setMyLocationEnabled(true);
googleMap.getUiSettings().setMyLocationButtonEnabled(false);
transaction.replace(R.id.fragment_container, tempFragment);
transaction.addToBackStack(null);
transaction.commit();
}
noFragments = false;
}
public void backToMap() {
noFragments = true;
this.hideKeyboard();
fragmentManager.popBackStack(firstStackTag,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
this.removeEditTextFocus(null);
this.setCorrectIcons();
this.displayMap();
}
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int id, long arg3) {
if (adapter.getResultSize() == 0) {
toast.setText(message);
toast.show();
} else {
String selection = editText.getText().toString();
if (id < adapter.getCount()) {
selection = adapter.getItem(id).getName();
}
this.hideKeyboard();
this.removeEditTextFocus(selection);
this.backToMap();
}
}
public void displayMap() {
// check if is Image ready
if (!campusMapView.isImageReady()) {
Message msg = mHandler.obtainMessage(MSG_DISPLAY_MAP);
mHandler.sendMessageDelayed(msg, DELAY_INIT_LAYOUT);
} else {
// get text from auto complete text box
String key = editText.getText().toString();
// get Marker object if exists
com.mrane.data.Marker marker = data.get(key);
// display and zoom to marker if exists
if (marker != null) {
Message msg = mHandler.obtainMessage(MSG_ANIMATE, key);
mHandler.sendMessageDelayed(msg, DELAY_ANIMATE);
} else {
campusMapView.setResultMarker(null);
this.dismissCard();
campusMapView.invalidate();
}
}
}
private void showResultOnMap(String key) {
com.mrane.data.Marker marker = data.get(key);
showCard(marker);
campusMapView.setAndShowResultMarker(marker);
}
public void showCard() {
com.mrane.data.Marker marker = campusMapView.getResultMarker();
showCard(marker);
}
public void showCard(com.mrane.data.Marker marker) {
String name = marker.getName();
if (!marker.getShortName().equals("0"))
name = marker.getShortName();
placeNameTextView.setText(name);
setSubHeading(marker);
setAddMarkerIcon(marker);
addDescriptionView(marker);
placeColor.setImageDrawable(new ColorDrawable(marker.getColor()));
getActivity().findViewById(R.id.place_group_color).setBackgroundColor(
marker.getColor());
getActivity().findViewById(R.id.dragView).setVisibility(View.VISIBLE);
reCenterMarker(marker);
cardSlideListener.showCard();
}
private void setImage(LinearLayout parent, com.mrane.data.Marker marker) {
View v = getLayoutInflater().inflate(R.layout.map_card_image, parent);
ImageView iv = (ImageView) v.findViewById(R.id.place_image);
int imageId = getResources().getIdentifier(marker.getImageUri(),
"drawable", getContext().getPackageName());
iv.setImageResource(imageId);
}
private void addDescriptionView(com.mrane.data.Marker marker) {
LinearLayout parent = (LinearLayout) getActivity().findViewById(R.id.other_details);
parent.removeAllViews();
if (!marker.getImageUri().isEmpty()) {
setImage(parent, marker);
}
if (marker instanceof Building) {
setChildrenView(parent, (Building) marker);
}
if (!marker.getDescription().isEmpty()) {
View desc = getLayoutInflater().inflate(R.layout.map_place_description,
parent);
TextView descHeader = (TextView) desc
.findViewById(R.id.desc_header);
Typeface regular = Typeface.createFromAsset(getContext().getAssets(),
FONT_REGULAR);
descHeader.setTypeface(regular, Typeface.BOLD);
TextView descContent = (TextView) desc
.findViewById(R.id.desc_content);
descContent.setTypeface(regular);
descContent.setText(getDescriptionText(marker));
Linkify.addLinks(descContent, Linkify.ALL);
descContent.setLinkTextColor(Color.rgb(19, 140, 190));
}
locationButton.setBackgroundTintList(ColorStateList.valueOf(Color.parseColor("#FFFFFF")));
//Initialize Google Play Services
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(getActivity(),
Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
//Location Permission already granted
buildGoogleApiClient();
googleMap.setMyLocationEnabled(true);
}
public static void setListViewHeightBasedOnChildren(ListView listView) {
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null)
return;
int desiredWidth = View.MeasureSpec.makeMeasureSpec(listView.getWidth(),
View.MeasureSpec.UNSPECIFIED);
int totalHeight = 0;
View view = null;
for (int i = 0; i < listAdapter.getCount(); i++) {
view = listAdapter.getView(i, view, listView);
if (i == 0)
view.setLayoutParams(new ViewGroup.LayoutParams(desiredWidth,
RelativeLayout.LayoutParams.WRAP_CONTENT));
view.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED);
totalHeight += view.getMeasuredHeight();
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
params.height = totalHeight
+ (listView.getDividerHeight() * (listAdapter.getCount() - 1));
listView.setLayoutParams(params);
listView.requestLayout();
}
private void setChildrenView(LinearLayout parent, Building building) {
View childrenView = getLayoutInflater().inflate(R.layout.map_children_view,
parent);
View headerLayout = childrenView.findViewById(R.id.header_layout);
TextView headerName = (TextView) childrenView
.findViewById(R.id.list_header);
String headerText = "inside ";
if (building.getShortName().equals("0"))
headerText += building.getName();
else
headerText += building.getShortName();
Typeface bold = Typeface.createFromAsset(getContext().getAssets(), FONT_REGULAR);
headerName.setTypeface(bold, Typeface.BOLD);
headerName.setText(headerText);
final ImageView icon = (ImageView) childrenView
.findViewById(R.id.arrow_icon);
final ListView childrenListView = (ListView) childrenView
.findViewById(R.id.child_list);
childrenListView.setVisibility(View.GONE);
ArrayList<String> childNames = new ArrayList<String>();
for (String name : building.children) {
childNames.add(name);
}
final CustomListAdapter adapter = new CustomListAdapter(getContext(),
R.layout.map_child, childNames);
childrenListView.setAdapter(adapter);
childrenListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1,
int position, long arg3) {
String key = adapter.getItem(position);
removeEditTextFocus(key);
backToMap();
}
});
headerLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
if (childrenListView.getVisibility() == View.VISIBLE) {
childrenListView.setVisibility(View.GONE);
icon.setImageResource(R.drawable.ic_action_next_item);
} else {
setListViewHeightBasedOnChildren(childrenListView);
childrenListView.setVisibility(View.VISIBLE);
icon.setImageResource(R.drawable.ic_action_expand);
}
}
});
}
private class CustomListAdapter extends ArrayAdapter<String> {
private Context mContext;
private int id;
private List<String> items;
public CustomListAdapter(Context context, int textViewResourceId,
List<String> list) {
super(context, textViewResourceId, list);
mContext = context;
id = textViewResourceId;
items = list;
}
@Override
public View getView(int position, View v, ViewGroup parent) {
View mView = v;
if (mView == null) {
LayoutInflater vi = (LayoutInflater) mContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mView = vi.inflate(id, null);
}
TextView text = (TextView) mView.findViewById(R.id.child_name);
Log.d("testing", "position = " + position);
if (items.get(position) != null) {
Typeface regular = Typeface.createFromAsset(getContext().getAssets(),
FONT_REGULAR);
text.setText(items.get(position));
text.setTypeface(regular);
}
return mView;
}
}
private SpannableStringBuilder getDescriptionText(com.mrane.data.Marker marker) {
String text = marker.getDescription();
SpannableStringBuilder desc = new SpannableStringBuilder(text);
String[] toBoldParts = { "Email", "Phone No.", "Fax No." };
for (String part : toBoldParts) {
setBold(desc, part);
}
return desc;
}
private void setBold(SpannableStringBuilder text, String part) {
int start = text.toString().indexOf(part);
int end = start + part.length();
final StyleSpan bold = new StyleSpan(Typeface.BOLD);
if (start >= 0)
text.setSpan(bold, start, end,
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private void setSubHeading(com.mrane.data.Marker marker) {
SpannableStringBuilder result = new SpannableStringBuilder("");
result.append(marker.getName());
if (marker instanceof Room) {
Room room = (Room) marker;
String tag = room.tag;
if (!tag.equals("Inside")) {
tag += ",";
} else {
//Request Location Permission
checkLocationPermission();
tag = "in";
}
com.mrane.data.Marker parent = data.get(room.parentKey);
final String parentKey = parent.getName();
String parentName = parent.getName();
if (!parent.getShortName().equals("0"))
parentName = parent.getShortName();
result.append(" - " + tag + " ");
int start = result.length();
result.append(parentName);
int end = result.length();
result.append(" ");
ClickableSpan parentSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
editText.setText(parentKey);
displayMap();
}
@Override
public void updateDrawState(TextPaint p) {
p.setColor(Color.rgb(19, 140, 190));
p.setUnderlineText(true);
}
};
result.setSpan(parentSpan, start, end,
SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
ClickableSpan restSpan1 = new ClickableSpan() {
private TextPaint ds;
@Override
public void onClick(View widget) {
updateDrawState(ds);
widget.invalidate();
// newCardTouchListener.toggleExpansion();
}
@Override
public void updateDrawState(TextPaint ds) {
ds.bgColor = Color.TRANSPARENT;
ds.setUnderlineText(false);
this.ds = ds;
}
};
ClickableSpan restSpan2 = new ClickableSpan() {
private TextPaint ds;
@Override
public void onClick(View widget) {
updateDrawState(ds);
widget.invalidate();
// newCardTouchListener.toggleExpansion();
}
@Override
public void updateDrawState(TextPaint ds) {
ds.bgColor = Color.TRANSPARENT;
ds.setUnderlineText(false);
this.ds = ds;
}
};
result.setSpan(restSpan1, 0, start,
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
result.setSpan(restSpan2, end, end + 1,
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
placeSubHeadTextView.setMovementMethod(LinkMovementMethod
.getInstance());
// placeSubHeadTextView.setHighlightColor(Color.TRANSPARENT);
placeSubHeadTextView.setOnClickListener(null);
} else {
buildGoogleApiClient();
googleMap.setMyLocationEnabled(true);
placeSubHeadTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// newCardTouchListener.toggleExpansion();
}
});
}
googleMap.getUiSettings().setZoomGesturesEnabled(true);
LatLngBounds iitbBounds = new LatLngBounds(new LatLng(19.1249000, 72.9046000), new LatLng(19.143522, 72.920000));
googleMap.setLatLngBoundsForCameraTarget(iitbBounds);
googleMap.setMaxZoomPreference(30);
googleMap.setMinZoomPreference(14.5f);
// Position the map's camera near Mumbai
LatLng iitb = new LatLng(19.1334, 72.9133);
googleMap.moveCamera(CameraUpdateFactory.newLatLng(iitb));
}
private void checkLocationPermission() {
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(),
Manifest.permission.ACCESS_FINE_LOCATION)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
new AlertDialog.Builder(getActivity())
.setTitle("Location Permission Needed")
.setMessage("This app needs the Location permission, please accept to use location functionality")
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//Prompt the user once explanation has been shown
requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
Constants.MY_PERMISSIONS_REQUEST_LOCATION);
}
})
.create()
.show();
placeSubHeadTextView.setText(result);
}
private Drawable getLockIcon(com.mrane.data.Marker marker) {
int color = marker.getColor();
int drawableId = R.drawable.lock_all_off;
if (campusMapView.isAddedMarker(marker)) {
if (color == com.mrane.data.Marker.COLOR_BLUE)
drawableId = R.drawable.lock_blue_on;
else if (color == com.mrane.data.Marker.COLOR_YELLOW)
drawableId = R.drawable.lock_on_yellow;
else if (color == com.mrane.data.Marker.COLOR_GREEN)
drawableId = R.drawable.lock_green_on;
else if (color == com.mrane.data.Marker.COLOR_GRAY)
drawableId = R.drawable.lock_gray_on;
}
Drawable lock = getResources().getDrawable(drawableId);
return lock;
}
public void expandCard() {
reCenterMarker();
}
private void reCenterMarker() {
com.mrane.data.Marker marker = campusMapView.getResultMarker();
reCenterMarker(marker);
}
private void reCenterMarker(com.mrane.data.Marker marker) {
PointF p = marker.getPoint();
float shift = getResources().getDimension(R.dimen.expanded_card_height) / 2.0f;
PointF center = new PointF(p.x, p.y + shift);
SubsamplingScaleImageView.AnimationBuilder anim = campusMapView.animateCenter(center);
anim.start();
}
public boolean removeMarker() {
if (campusMapView.getResultMarker() == null) {
return false;
} else {
if (slidingLayout.isPanelExpanded()
|| slidingLayout.isPanelAnchored()) {
slidingLayout.collapsePanel();
} else {
// No explanation needed, we can request the permission.
requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
Constants.MY_PERMISSIONS_REQUEST_LOCATION);
editText.setText("");
campusMapView.setResultMarker(null);
dismissCard();
}
return true;
}
}
protected synchronized void buildGoogleApiClient() {
mGoogleApiClient = new GoogleApiClient.Builder(getActivity())
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(LocationServices.API)
.build();
mGoogleApiClient.connect();
/**
* Hides the card
*
* @return true if the card was visible while this function was called
*/
public void dismissCard() {
cardSlideListener.dismissCard();
campusMapView.invalidate();
}
@Override
public void onLocationChanged(Location location) {
mLastLocation = location;
if (mCurrLocationMarker != null) {
mCurrLocationMarker.remove();
public void removeClick(View v) {
this.editText.setText("");
displayMap();
}
public void indexClick(View v) {
this.putFragment(indexFragment);
this.removeEditTextFocus(null);
this.setCorrectIcons();
}
public void mapClick(View v) {
this.backToMap();
this.removeEditTextFocus("");
}
private void removeEditTextFocus(String text) {
if (this.editTextFocused) {
this.hideKeyboard();
editText.clearFocus();
}
//Place current location marker
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
//move map camera
googleMap.moveCamera(CameraUpdateFactory.newLatLng(latLng));
googleMap.animateCamera(CameraUpdateFactory.zoomTo(17));
if (text == null) {
//stop location updates
if (mGoogleApiClient != null) {
LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
} else if (text.equals("")) {
this.setOldText();
} else {
editText.setText(text);
}
}
@Override
public void onConnectionSuspended(int i) {
public FuzzySearchAdapter getAdapter() {
return adapter;
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case Constants.MY_PERMISSIONS_REQUEST_LOCATION: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
if (mGoogleApiClient == null) {
buildGoogleApiClient();
}
googleMap.setMyLocationEnabled(true);
}
private void setOldText() {
com.mrane.data.Marker oldMarker = campusMapView.getResultMarker();
if (oldMarker == null) {
if (editText.length() > 0) {
editText.getText().clear();
}
} else {
editText.setText(oldMarker.getName());
}
}
private void setCorrectIcons() {
if (noFragments) {
if (this.handleRemoveIcon()) {
this.noIndexButton();
} else {
this.setVisibleButton(indexIcon);
}
} else {
if (fragment instanceof ListFragment) {
if (this.handleRemoveIcon()) {
this.noIndexButton();
} else {
this.setVisibleButton(indexIcon);
}
return;
} else if (fragment instanceof IndexFragment) {
this.setVisibleButton(mapIcon);
}
}
}
private void noIndexButton() {
indexIcon.setVisibility(View.GONE);
mapIcon.setVisibility(View.GONE);
}
private boolean handleRemoveIcon() {
String text = editText.getText().toString();
if (text.isEmpty() || text.equals(null)) {
removeIcon.setVisibility(View.GONE);
return false;
} else {
removeIcon.setVisibility(View.VISIBLE);
return true;
}
}
private void setVisibleButton(ImageButton icon) {
indexIcon.setVisibility(View.GONE);
mapIcon.setVisibility(View.GONE);
icon.setVisibility(View.VISIBLE);
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
public void onFocusChange(View v, boolean focus) {
this.editTextFocused = focus;
if (focus) {
this.putFragment(listFragment);
fragmentContainer.setOnTouchListener(this);
String text = editText.getText().toString()
.toLowerCase(Locale.getDefault());
adapter.filter(text);
} else {
fragmentContainer.setOnTouchListener(null);
}
this.setCorrectIcons();
}
private void hideKeyboard() {
InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
//Find the currently focused view, so we can grab the correct window token from it.
View view = getActivity().getCurrentFocus();
//If no view currently has focus, create a new one, just so we can grab a window token from it
if (view == null) {
view = new View(getActivity());
}
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
@Override
public void onConnected(Bundle bundle) {
mLocationRequest = new LocationRequest();
mLocationRequest.setInterval(1000);
mLocationRequest.setFastestInterval(1000);
mLocationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
if (ContextCompat.checkSelfPermission(getActivity(),
Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, mLocationRequest, this);
public boolean onTouch(View arg0, MotionEvent arg1) {
if (adapter.getResultSize() != 0) {
removeEditTextFocus(null);
}
return false;
}
public void addMarkerClick(View v) {
campusMapView.toggleMarker();
setAddMarkerIcon();
}
public void playAnimSound(int sound_index) {
if (sound_index >= 0 && sound_index < soundPoolIds.length) {
if (!settingsManager.isMuted()) {
soundPool.play(soundPoolIds[sound_index], 1.0f, 1.0f, 1, 0, 1f);
}
}
}
public void playAnimSoundDelayed(int sound_index, long delay) {
Message msg = mHandler.obtainMessage(MSG_PLAY_SOUND, sound_index, 0);
mHandler.sendMessageDelayed(msg, delay);
}
private void setAddMarkerIcon() {
setAddMarkerIcon(campusMapView.getResultMarker());
}
private void setAddMarkerIcon(com.mrane.data.Marker m) {
addMarkerIcon.setImageDrawable(getLockIcon(m));
}
@Override
public boolean onChildClick(ExpandableListView parent, View v,
int groupPosition, int childPosition, long id) {
String selection = (String) expAdapter.getChild(groupPosition,
childPosition);
this.hideKeyboard();
this.removeEditTextFocus(selection);
this.backToMap();
return true;
}
public void setExpAdapter(ExpandableListAdapter expAdapter) {
this.expAdapter = expAdapter;
}
private static final String INSTANCE_CARD_STATE = "instanceCardState";
private static final String INSTANCE_VISIBILITY_INDEX = "instanceVisibilityIndex";
public SlidingUpPanelLayout getSlidingLayout() {
return slidingLayout;
}
}
......@@ -146,7 +146,7 @@ public class MessMenuFragment extends BaseFragment {
private void displayMessMenu(HostelMessMenu hostelMessMenu) {
/* Skip if we're already destroyed */
if (getActivity() == null) return;
if (getActivity() == null || getView() == null) return;
List<MessMenu> messMenus = hostelMessMenu.getMessMenus();
......
......@@ -98,7 +98,7 @@ public class MyEventsFragment extends BaseFragment {
private void displayEvents(final List<Event> events) {
/* Check if already destroyed */
if (getActivity() == null) return;
if (getActivity() == null || getView() == null) return;
final FeedAdapter feedAdapter = new FeedAdapter(events, new ItemClickListener() {
@Override
......
......@@ -99,7 +99,7 @@ public class NewsFragment extends BaseFragment {
private void displayNews(final List<NewsArticle> result) {
/* Skip if we're already destroyed */
if (getActivity() == null) return;
if (getActivity() == null || getView() == null) return;
final NewsAdapter newsAdapter = new NewsAdapter(result, new ItemClickListener() {
@Override
......
......@@ -100,7 +100,7 @@ public class PlacementBlogFragment extends BaseFragment {
private void displayPlacementFeed(final List<PlacementBlogPost> result) {
/* Skip if we're already destroyed */
if (getActivity() == null) return;
if (getActivity() == null || getView() == null) return;
final PlacementBlogAdapter placementBlogAdapter = new PlacementBlogAdapter(result, new ItemClickListener() {
@Override
......
......@@ -100,7 +100,7 @@ public class TrainingBlogFragment extends BaseFragment {
private void displayTrainingFeed(final List<TrainingBlogPost> result) {
/* Skip if we're already destroyed */
if (getActivity() == null) return;
if (getActivity() == null || getView() == null) return;
final TrainingBlogAdapter trainingBlogAdapter = new TrainingBlogAdapter(result, new ItemClickListener() {
@Override
......
package com.mrane.campusmap;
import android.content.Context;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.mrane.data.Marker;
import java.util.HashMap;
import java.util.List;
import app.insti.R;
import app.insti.fragment.MapFragment;
public class ExpandableListAdapter extends BaseExpandableListAdapter {
private Context _context;
private List<String> _listDataHeader;
private HashMap<String, List<String>> _listDataChild;
public ExpandableListAdapter(Context context, List<String> listDataHeader,
HashMap<String, List<String>> listChildData) {
this._context = context;
this._listDataHeader = listDataHeader;
this._listDataChild = listChildData;
}
@Override
public Object getChild(int groupPosition, int childPosititon) {
return this._listDataChild.get(this._listDataHeader.get(groupPosition))
.get(childPosititon);
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
@Override
public View getChildView(int groupPosition, final int childPosition,
boolean isLastChild, View convertView, ViewGroup parent) {
String headerTitle = (String) getGroup(groupPosition);
final String childText = (String) getChild(groupPosition, childPosition);
if (convertView == null) {
LayoutInflater infalInflater = (LayoutInflater) this._context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = infalInflater.inflate(R.layout.map_list_item, null);
}
TextView txtListChild = (TextView) convertView
.findViewById(R.id.lblListItem);
View itemGroupColor = (View) convertView.findViewById(R.id.item_group_color);
int color = Marker.getColor(Marker.getGroupId(headerTitle));
itemGroupColor.setBackgroundColor(color);
Typeface regular = Typeface.createFromAsset(_context.getAssets(), MapFragment.FONT_REGULAR);
txtListChild.setTypeface(regular);
txtListChild.setText(childText);
return convertView;
}
@Override
public int getChildrenCount(int groupPosition) {
return this._listDataChild.get(this._listDataHeader.get(groupPosition))
.size();
}
@Override
public Object getGroup(int groupPosition) {
return this._listDataHeader.get(groupPosition);
}
@Override
public int getGroupCount() {
return this._listDataHeader.size();
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded,
View convertView, ViewGroup parent) {
String headerTitle = (String) getGroup(groupPosition);
if (convertView == null) {
LayoutInflater infalInflater = (LayoutInflater) this._context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = infalInflater.inflate(
R.layout.map_expandable_list_header, null);
}
TextView lblListHeader = (TextView) convertView
.findViewById(R.id.lblListHeader);
Typeface regular = Typeface.createFromAsset(_context.getAssets(), MapFragment.FONT_REGULAR);
lblListHeader.setTypeface(regular);
lblListHeader.setText(headerTitle);
ImageView iconExpand = (ImageView) convertView
.findViewById(R.id.icon_expand);
ImageView groupColor = (ImageView) convertView.findViewById(R.id.group_color);
int color = Marker.getColor(Marker.getGroupId(headerTitle));
groupColor.setImageDrawable(new ColorDrawable(color));
if (isExpanded) {
iconExpand.setImageResource(R.drawable.ic_action_expand);
} else {
iconExpand.setImageResource(R.drawable.ic_action_next_item);
}
return convertView;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
}
package com.mrane.campusmap;
import android.content.Context;
import android.graphics.Typeface;
import android.text.Html;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.mrane.data.Marker;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import app.insti.R;
import app.insti.fragment.MapFragment;
public class FuzzySearchAdapter extends BaseAdapter {
Context mContext;
LayoutInflater inflater;
private List<Marker> resultlist = null;
private ArrayList<Marker> inputlist;
private List<ScoredMarker> map;
private Locale l;
private String searchedText = "";
private SettingsManager settingsManager;
private boolean turnOnResidences;
public FuzzySearchAdapter(Context context, List<Marker> inputlist) {
mContext = context;
l = Locale.getDefault();
this.resultlist = inputlist;
Collections.sort(resultlist, new MarkerNameComparator());
inflater = LayoutInflater.from(mContext);
this.inputlist = new ArrayList<Marker>();
this.inputlist.addAll(resultlist);
map = new ArrayList<ScoredMarker>();
}
public class ViewHolder {
TextView label;
LinearLayout rowContainer;
}
public class ScoredMarker {
Marker m;
int score;
public ScoredMarker(int score, Marker m) {
this.m = m;
this.score = score;
}
}
public int getResultSize() {
return resultlist.size();
}
@Override
public int getCount() {
if (this.getResultSize() == 0) {
return 1;
}
return resultlist.size();
}
@Override
public Marker getItem(int position) {
return resultlist.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View view, ViewGroup parent) {
final ViewHolder holder;
if (view == null) {
holder = new ViewHolder();
view = inflater.inflate(R.layout.map_row_layout, null);
holder.label = (TextView) view.findViewById(R.id.label);
Typeface regular = Typeface.createFromAsset(mContext.getAssets(),
MapFragment.FONT_REGULAR);
holder.label.setTypeface(regular);
holder.rowContainer = (LinearLayout) view
.findViewById(R.id.row_container);
view.setTag(holder);
} else {
holder = (ViewHolder) view.getTag();
}
// Set the results into TextViews
if (this.getResultSize() == 0) {
if (settingsManager.showResidences()) {
holder.label.setText("Sorry, no results found.");
} else {
if (turnOnResidences) {
holder.label
.setText("There are results in residences. Select show residences from settings.");
} else {
holder.label.setText("Sorry, no results found.");
}
}
} else {
holder.label.setText(getSpannedText(resultlist.get(position)
.getName(), searchedText));
}
return view;
}
private Spanned getSpannedText(String name, String searchedText2) {
String smallLetterName = name.toLowerCase(l);
searchedText2 = searchedText2.toLowerCase(l).replaceAll("\\s", "");
if (searchedText2.equals("")) {
return Html.fromHtml(name);
} else {
String htmlName = "";
if (isShortForm(smallLetterName, searchedText2)) {
htmlName = getHighlightedShortform(name, searchedText2);
} else {
int i = 0;
int j = 0;
while (i < searchedText2.length()) {
if (smallLetterName.charAt(j) == searchedText2.charAt(i)) {
htmlName += "<b>" + name.charAt(j) + "</b>";
i++;
j++;
} else {
htmlName += name.charAt(j);
j++;
}
}
if (j < name.length()) {
htmlName += name.substring(j, name.length());
}
}
return Html.fromHtml(htmlName);
}
}
private String getHighlightedShortform(String name, String searchedText2) {
String smallCapsName = name.toLowerCase(l);
String possibleShortform = "";
for (int i = 0; i < searchedText2.length(); i++) {
possibleShortform = "";
for (int j = 0; j < searchedText2.length(); j++) {
if (j <= i) {
possibleShortform += searchedText2.charAt(j);
} else {
possibleShortform += "(.*)" + " " + searchedText2.charAt(j);
}
}
possibleShortform += "(.*)";
if (smallCapsName.matches(possibleShortform)) {
name = makeBold(name, searchedText2.substring(0, i + 1));
i++;
while (i < searchedText2.length()) {
name = makeBold(name, " " + searchedText2.charAt(i));
i++;
}
}
}
return name;
}
private String makeBold(String name, String substring) {
String smallCapsName = name.toLowerCase(l);
int firstIndex = smallCapsName.indexOf(substring);
if (name.charAt(firstIndex) == ' ') {
name = name.substring(0, firstIndex + 1)
+ "<b>"
+ name.substring(firstIndex + 1,
firstIndex + substring.length())
+ "</b>"
+ name.substring(firstIndex + substring.length(),
name.length());
} else {
name = name.substring(0, firstIndex)
+ "<b>"
+ name.substring(firstIndex,
firstIndex + substring.length())
+ "</b>"
+ name.substring(firstIndex + substring.length(),
name.length());
}
return name;
}
public void filter(String charText) {
turnOnResidences = false;
charText = charText.toLowerCase(Locale.getDefault());
searchedText = charText;
resultlist.clear();
map.clear();
if (charText.length() == 0) {
if (settingsManager.showResidences()) {
resultlist.addAll(inputlist);
} else {
for (Marker m : inputlist) {
if (m.getGroupIndex() != Marker.RESIDENCES) {
resultlist.add(m);
}
}
}
} else if (charText.length() > 10) {
for (Marker m : inputlist) {
if (m.getName().toLowerCase(Locale.getDefault())
.contains(charText)) {
if (settingsManager.showResidences()) {
resultlist.add(m);
} else {
if (m.getGroupIndex() != Marker.RESIDENCES) {
resultlist.add(m);
} else {
turnOnResidences = true;
}
}
}
}
} else {
for (Marker m : inputlist) {
int score = checkModifyMarker(m, charText);
if (score != 0) {
if (settingsManager.showResidences()) {
map.add(new ScoredMarker(score, m));
} else {
if (m.getGroupIndex() != Marker.RESIDENCES) {
map.add(new ScoredMarker(score, m));
} else {
turnOnResidences = true;
}
}
}
}
resultlist = sortByScore(map);
}
notifyDataSetChanged();
}
private List<Marker> sortByScore(List<ScoredMarker> tempScore) {
List<Marker> templist = new ArrayList<Marker>();
Collections.sort(tempScore, new MarkerScoreComparator());
for (ScoredMarker k : tempScore) {
templist.add(k.m);
}
return templist;
}
private int checkModifyMarker(Marker m, String charText) {
int tempScore = 5;
String tempCharText = "(.*)";
for (int i = 0; i < charText.length(); i++) {
tempCharText += charText.charAt(i) + "(.*)";
}
if (m.getName().toLowerCase(l).matches(tempCharText)) {
boolean b = false;
if (m.getName().toLowerCase(l).startsWith(charText)) {
return 1;
}
if (isShortForm(m.getName(), charText)) {
return 2;
}
for (String s : m.getName().split(" ")) {
b = b || s.toLowerCase(l).startsWith("" + charText.charAt(0));
if (!b) {
tempScore += 10;
}
if (s.startsWith(charText)) {
return 3;
}
}
if (b) {
return tempScore;
} else {
return 0;
}
} else {
return 0;
}
}
private boolean isShortForm(String name, String charText) {
name = name.toLowerCase(l);
charText = charText.toLowerCase(l).replaceAll("\\s", "");
String possibleShortform = "";
for (int i = 0; i < charText.length(); i++) {
possibleShortform = "";
for (int j = 0; j < charText.length(); j++) {
if (j <= i) {
possibleShortform += charText.charAt(j);
} else {
possibleShortform += "(.*)" + " " + charText.charAt(j);
}
}
possibleShortform += "(.*)";
if (name.matches(possibleShortform)) {
return true;
}
}
return false;
}
public SettingsManager getSettingsManager() {
return settingsManager;
}
public void setSettingsManager(SettingsManager settingsManager) {
this.settingsManager = settingsManager;
}
public class MarkerScoreComparator implements Comparator<ScoredMarker> {
public int compare(ScoredMarker m1, ScoredMarker m2) {
return m1.score - m2.score;
}
}
public class MarkerNameComparator implements Comparator<Marker> {
public int compare(Marker m1, Marker m2) {
return m1.getName().toLowerCase(l)
.compareTo(m2.getName().toLowerCase(l));
}
}
}
package com.mrane.campusmap;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnGroupClickListener;
import android.widget.ExpandableListView.OnGroupCollapseListener;
import android.widget.ExpandableListView.OnGroupExpandListener;
import com.mrane.data.Marker;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import app.insti.R;
import app.insti.fragment.MapFragment;
public class IndexFragment extends Fragment implements OnGroupExpandListener,
OnGroupCollapseListener, OnGroupClickListener {
MapFragment mainActivity;
ExpandableListAdapter adapter;
HashMap<String, Marker> data;
View rootView;
ExpandableListView list;
List<String> headers = new ArrayList<String>();
HashMap<String, List<String>> childData = new HashMap<String, List<String>>();
int pos;
int prevGroup = -1;
public IndexFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mainActivity = MapFragment.getMainActivity();
data = mainActivity.data;
if (headers.isEmpty()) {
setHeaderAndChildData();
}
adapter = new ExpandableListAdapter(mainActivity.getContext(), headers, childData);
rootView = inflater.inflate(R.layout.map_index_fragment, container, false);
list = (ExpandableListView) rootView.findViewById(R.id.index_list);
mainActivity.setExpAdapter(adapter);
list.setAdapter(adapter);
list.setOnChildClickListener(mainActivity);
list.setOnGroupExpandListener(this);
list.setOnGroupCollapseListener(this);
list.setOnGroupClickListener(this);
return rootView;
}
@Override
public void onResume() {
String name = mainActivity.editText.getText().toString();
if(name.isEmpty()) {
collapseAllGroups();
list.setSelectedGroup(0);
} else {
if(data.containsKey(name)) {
String groupName = data.get(name).getGroupName();
int groupId = getGroupId(groupName);
if(groupId != -1) {
list.expandGroup(groupId);
list.setSelectedGroup(groupId);
}
}
}
super.onResume();
}
private int getGroupId(String groupName) {
int groupCount = adapter.getGroupCount();
int temp = -1;
for (int i=0; i < groupCount; i++) {
if(adapter.getGroup(i).equals(groupName)) {
temp = i;
}
}
return temp;
}
private void collapseAllGroups() {
int groupCount = adapter.getGroupCount();
for (int i=0; i < groupCount; i++) {
list.collapseGroup(i);
}
}
private void setChildData() {
Collection<Marker> keys = data.values();
for (Marker key : keys) {
List<String> child = childData.get(key.getGroupName());
child.add(key.getName());
}
sortChildData();
}
private void sortChildData() {
for (String header : headers) {
List<String> child = childData.get(header);
Collections.sort(child);
}
}
private void setHeaderAndChildData() {
String[] headerString = Marker.getGroupNames();
Collections.addAll(headers, headerString);
for (String header : headers) {
childData.put(header, new ArrayList<String>());
}
setChildData();
}
@Override
public void onGroupExpand(int groupPosition) {
if (prevGroup != -1 && prevGroup != groupPosition) {
list.collapseGroup(prevGroup);
}
prevGroup = groupPosition;
}
@Override
public void onGroupCollapse(int groupPosition) {
if (prevGroup != -1) {
//list.setSelectionFromTop(prevGroup, 0);
}
}
@Override
public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition,
long id) {
// Implement this method to scroll to the correct position as this doesn't
// happen automatically if we override onGroupExpand() as above
parent.smoothScrollToPosition(groupPosition);
// Need default behaviour here otherwise group does not get expanded/collapsed
// on click
if (parent.isGroupExpanded(groupPosition)) {
parent.collapseGroup(groupPosition);
} else {
parent.expandGroup(groupPosition);
}
return true;
}
}
package com.mrane.campusmap;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import com.mrane.data.Marker;
import java.util.HashMap;
import app.insti.R;
import app.insti.fragment.MapFragment;
public class ListFragment extends Fragment {
MapFragment mainActivity;
FuzzySearchAdapter adapter;
HashMap<String, Marker> data;
View rootView;
ListView list;
public ListFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mainActivity = MapFragment.getMainActivity();
adapter = mainActivity.getAdapter();
rootView = inflater.inflate(R.layout.map_list_fragment, container, false);
list = (ListView) rootView.findViewById(R.id.suggestion_list);
list.setAdapter(adapter);
list.setOnItemClickListener(mainActivity);
list.setOnTouchListener(mainActivity);
list.setFastScrollEnabled(true);
return rootView;
}
}
package com.mrane.campusmap;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import app.insti.R;
public class SettingsManager implements OnSharedPreferenceChangeListener{
private SharedPreferences sharedPrefs;
private String muteKey;
private String residencesKey;
private String lastUpdatedKey;
public SettingsManager(Context context){
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
sharedPrefs.registerOnSharedPreferenceChangeListener(this);
Resources res = context.getResources();
muteKey = "mute";
residencesKey = "residences";
lastUpdatedKey = "lastupdated";
}
public boolean isMuted(){
return sharedPrefs.getBoolean(muteKey, false);
}
public boolean showResidences(){
return sharedPrefs.getBoolean(residencesKey, true);
}
public long getLastUpdatedOn() {
return sharedPrefs.getLong(lastUpdatedKey, 0);
}
public void setLastUpdatedOn(long lastUpdatedOn) {
sharedPrefs.edit().putLong(lastUpdatedKey, lastUpdatedOn).commit();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
}
}
package com.mrane.data;
public class Building extends Marker {
public String[] children;
public Building(String name, String shortName, float x, float y,
int groupIndex, String[] children, String description) {
super(name, shortName, x, y, groupIndex, description);
this.children = children;
}
public String[] getChildren() {
return children;
}
public void setChildren(String[] children) {
this.children = children;
}
}
package com.mrane.data;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import app.insti.data.Venue;
public class Locations {
public HashMap<String, Marker> data = new HashMap<String, Marker>();
public Locations(List<Venue> venueList) {
// Add locations
for (Venue venue : venueList) {
Marker marker;
// Skip bad locations
if (venue.getVenuePixelX() == 0 || venue.getVenueGroupId() == null) {
continue;
}
// Set some things up
if (venue.getVenueParentRelation() == null) {
venue.setVenueParentRelation("");
}
if (venue.getVenueParentRelation() == null || venue.getVenueParentRelation().equals("")) {
// Add children
final List<String> children = new ArrayList();
for (Venue child : venueList) {
if (child.getVenueParentId() != null && child.getVenueParentId().equals(venue.getVenueID())) {
children.add(child.getVenueName());
}
}
String[] childArray = new String[children.size()];
marker = new Building(
venue.getVenueName(), venue.getVenueShortName(), venue.getVenuePixelX(), venue.getVenuePixelY(),
venue.getVenueGroupId(), children.toArray(childArray), venue.getVenueDescripion());
} else {
// Get parent name
String parentName = "";
for (Venue parent : venueList) {
if (parent.getVenueID().equals(venue.getVenueParentId())) {
parentName = parent.getVenueName();
break;
}
}
marker = new Room(venue.getVenueName(), venue.getVenueShortName(), venue.getVenuePixelX(), venue.getVenuePixelY(),
venue.getVenueGroupId(), parentName, venue.getVenueParentRelation(), venue.getVenueDescripion());
}
data.put(marker.getName(), marker);
}
}
}
\ No newline at end of file
package com.mrane.data;
import android.graphics.Color;
import android.graphics.PointF;
import java.util.ArrayList;
import java.util.Arrays;
public class Marker {
private int id;
private String name;
private String shortName;
private PointF point;
private int groupIndex;
private boolean showDefault;
private String description;
private String tag;
private String imageUri;
private int parentId;
private String parentRel;
private int[] childIds;
private long lat;
private long lng;
public static final int COLOR_BLUE = Color.rgb(75, 186, 238);
public static final int COLOR_YELLOW = Color.rgb(255, 186, 0);
public static final int COLOR_GREEN = Color.rgb(162, 208, 104);
public static final int COLOR_GRAY = Color.rgb(156, 156, 156);
public static final int DEPARTMENTS = 1;
public static final int HOSTELS = 2;
public static final int RESIDENCES = 3;
public static final int HALLS_N_AUDITORIUMS = 4;
public static final int FOOD_STALLS = 5;
public static final int BANKS_N_ATMS = 6;
public static final int SCHOOLS = 7;
public static final int SPORTS = 8;
public static final int OTHERS = 9;
public static final int GATES = 10;
public static final int PRINT = 11;
public static final int LABS = 12;
private static final String DEPARTMENTS_NAME = "Departments";
private static final String HOSTELS_NAME = "Hostels";
private static final String RESIDENCES_NAME = "Residences";
private static final String HALLS_N_AUDITORIUMS_NAME = "Halls and Auditoriums";
private static final String FOOD_STALLS_NAME = "Food Stalls";
private static final String BANKS_N_ATMS_NAME = "Banks and Atms";
private static final String SCHOOLS_NAME = "Schools";
private static final String SPORTS_NAME = "Sports";
private static final String OTHERS_NAME = "Others";
private static final String GATES_NAME = "Gates";
private static final String PRINT_NAME = "Printer facility";
private static final String LABS_NAME = "Labs";
public Marker(String name, String shortName, float x, float y,
int groupIndex, String description) {
this.setPoint(new PointF(x, y));
this.groupIndex = groupIndex;
this.setName(name);
this.setShortName(shortName);
this.setShowDefault(false);
this.setDescription(description);
this.setImageUri("");
}
public Marker(int id, String name, String shortName, float pixelX, float pixelY,
int groupIndex, String description, int parentId, String parentRel,
int[] childIds, long lat, long lng) {
this.id = id;
this.name = name;
this.shortName = shortName;
this.point = new PointF(pixelX, pixelY);
this.groupIndex = groupIndex;
this.description = description;
this.setParentId(parentId);
this.setParentRel(parentRel);
this.setChildIds(childIds);
this.setLat(lat);
this.setLng(lng);
}
public static int getColor(int group) {
Integer[] yellowGroup = new Integer[] { HOSTELS };
Integer[] blueGroup = new Integer[] { DEPARTMENTS, LABS,
HALLS_N_AUDITORIUMS };
Integer[] greenGroup = new Integer[] { RESIDENCES };
Integer[] purpleGroup = new Integer[] { FOOD_STALLS, BANKS_N_ATMS,
SCHOOLS, SPORTS, OTHERS, GATES, PRINT };
ArrayList<Integer> yellowList = new ArrayList<Integer>(
Arrays.asList(yellowGroup));
ArrayList<Integer> blueList = new ArrayList<Integer>(
Arrays.asList(blueGroup));
ArrayList<Integer> greenList = new ArrayList<Integer>(
Arrays.asList(greenGroup));
ArrayList<Integer> purpleList = new ArrayList<Integer>(
Arrays.asList(purpleGroup));
if (yellowList.contains(group)) {
return COLOR_YELLOW;
} else if (blueList.contains(group)) {
return COLOR_BLUE;
} else if (greenList.contains(group)) {
return COLOR_GREEN;
} else if (purpleList.contains(group)) {
return COLOR_GRAY;
}
return 0;
}
public int getColor() {
int group = this.groupIndex;
return getColor(group);
}
public String getGroupName() {
switch (groupIndex) {
case DEPARTMENTS:
return DEPARTMENTS_NAME;
case HOSTELS:
return HOSTELS_NAME;
case RESIDENCES:
return RESIDENCES_NAME;
case HALLS_N_AUDITORIUMS:
return HALLS_N_AUDITORIUMS_NAME;
case FOOD_STALLS:
return FOOD_STALLS_NAME;
case BANKS_N_ATMS:
return BANKS_N_ATMS_NAME;
case SCHOOLS:
return SCHOOLS_NAME;
case SPORTS:
return SPORTS_NAME;
case OTHERS:
return OTHERS_NAME;
case GATES:
return GATES_NAME;
case PRINT:
return PRINT_NAME;
case LABS:
return LABS_NAME;
}
return "";
}
public static String[] getGroupNames() {
String[] groupNames = { DEPARTMENTS_NAME, LABS_NAME,
HALLS_N_AUDITORIUMS_NAME, HOSTELS_NAME, RESIDENCES_NAME,
FOOD_STALLS_NAME, BANKS_N_ATMS_NAME, SCHOOLS_NAME, SPORTS_NAME,
PRINT_NAME, GATES_NAME, OTHERS_NAME };
return groupNames;
}
public static int getGroupId(String groupName) {
int result = 0;
if (groupName.equals(DEPARTMENTS_NAME))
result = DEPARTMENTS;
if (groupName.equals(HOSTELS_NAME))
result = HOSTELS;
if (groupName.equals(RESIDENCES_NAME))
result = RESIDENCES;
if (groupName.equals(HALLS_N_AUDITORIUMS_NAME))
result = HALLS_N_AUDITORIUMS;
if (groupName.equals(FOOD_STALLS_NAME))
result = FOOD_STALLS;
if (groupName.equals(BANKS_N_ATMS_NAME))
result = BANKS_N_ATMS;
if (groupName.equals(SCHOOLS_NAME))
result = SCHOOLS;
if (groupName.equals(SPORTS_NAME))
result = SPORTS;
if (groupName.equals(OTHERS_NAME))
result = OTHERS;
if (groupName.equals(GATES_NAME))
result = GATES;
if (groupName.equals(PRINT_NAME))
result = PRINT;
if (groupName.equals(LABS_NAME))
result = LABS;
return result;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getShortName() {
return shortName;
}
public void setShortName(String shortName) {
this.shortName = shortName;
}
public PointF getPoint() {
return point;
}
public void setPoint(PointF point) {
this.point = point;
}
public int getGroupIndex() {
return groupIndex;
}
public void setGroupIndex(int groupIndex) {
this.groupIndex = groupIndex;
}
public boolean isShowDefault() {
return showDefault;
}
public void setShowDefault(boolean showDefault) {
this.showDefault = showDefault;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getTag() {
return tag;
}
public void setTag(String tag) {
this.tag = tag;
}
public String getImageUri() {
return imageUri;
}
public void setImageUri(String imageUri) {
this.imageUri = imageUri;
}
public int[] getChildIds() {
return childIds;
}
public void setChildIds(int[] childIds) {
this.childIds = childIds;
}
public int getParentId() {
return parentId;
}
public void setParentId(int parentId) {
this.parentId = parentId;
}
public String getParentRel() {
return parentRel;
}
public void setParentRel(String parentRel) {
this.parentRel = parentRel;
}
public long getLat() {
return lat;
}
public void setLat(long lat) {
this.lat = lat;
}
public long getLng() {
return lng;
}
public void setLng(long lng) {
this.lng = lng;
}
}
package com.mrane.data;
public class Room extends Marker {
public String parentKey;
public String tag;
public Room(String fullName, String shortName, float x, float y,
int groupId, String parentName, String tag, String desc) {
super(fullName, shortName, x, y, groupId, desc);
this.tag = tag;
this.parentKey = parentName;
}
}
package com.mrane.navigation;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import app.insti.R;
import app.insti.fragment.MapFragment;
import com.mrane.navigation.SlidingUpPanelLayout.PanelSlideListener;
public class CardSlideListener implements PanelSlideListener,
ValueAnimator.AnimatorUpdateListener {
private MapFragment mainActivity;
private SlidingUpPanelLayout slidingLayout;
private EndDetectScrollView scrollView;
private ValueAnimator animator;
private static final long TIME_ANIMATION_SHOW = 250;
public CardSlideListener(MapFragment mainActivity) {
this.mainActivity = mainActivity;
slidingLayout = mainActivity.getSlidingLayout();
scrollView = (EndDetectScrollView) mainActivity.getActivity()
.findViewById(R.id.new_expanded_place_card_scroll);
animator = new ValueAnimator();
animator.addUpdateListener(this);
Interpolator i = new Interpolator() {
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
animator.setInterpolator(i);
}
@Override
public void onPanelSlide(View panel, float slideOffset) {
// setActionBarTranslation(slidingLayout.getCurrentParalaxOffset());
// if(slideOffset >= slidingLayout.getAnchorPoint()){
// mainActivity.getSupportActionBar().hide();
// }
// else{
// mainActivity.getSupportActionBar().show();
// }
}
@Override
public void onPanelCollapsed(View panel) {
}
@Override
public void onPanelExpanded(View panel) {
scrollView.requestDisallowInterceptTouchEvent(false);
scrollView.setScrollingEnabled(true);
}
@Override
public void onPanelAnchored(View panel) {
scrollView.requestDisallowInterceptTouchEvent(true);
scrollView.setScrollingEnabled(false);
}
@Override
public void onPanelHidden(View panel) {
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void setActionBarTranslation(float y) {
// Figure out the actionbar height
int actionBarHeight = 20;
// A hack to add the translation to the action bar
ViewGroup content = ((ViewGroup) mainActivity.getActivity().findViewById(
android.R.id.content).getParent());
int children = content.getChildCount();
for (int i = 0; i < children; i++) {
View child = content.getChildAt(i);
if (child.getId() != android.R.id.content) {
if (y <= -actionBarHeight) {
child.setVisibility(View.GONE);
} else {
child.setVisibility(View.VISIBLE);
child.setTranslationY(y);
}
}
}
}
public void dismissCard() {
animator.cancel();
int initialPanelHeight = slidingLayout.getPanelHeight();
int finalPanelHeight = 0;
animator.setIntValues(initialPanelHeight, finalPanelHeight);
animator.setDuration(TIME_ANIMATION_SHOW);
animator.start();
}
public void showCard() {
animator.cancel();
int initialPanelHeight = slidingLayout.getPanelHeight();
int finalPanelHeight = mainActivity.getResources()
.getDimensionPixelSize(R.dimen.hidden_card_height);
animator.setIntValues(initialPanelHeight, finalPanelHeight);
animator.setDuration(TIME_ANIMATION_SHOW);
animator.start();
}
@Override
public void onAnimationUpdate(ValueAnimator animator) {
int panelHeight = (Integer) animator.getAnimatedValue();
slidingLayout.setPanelHeight(panelHeight);
}
}
package com.mrane.navigation;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ScrollView;
public class EndDetectScrollView extends ScrollView {
private enum ScrollState {
TOP, BETWEEN, BOTTOM
}
private ScrollState mCurrState = ScrollState.TOP;
private boolean scrollable = true;
public interface ScrollEndListener {
public void onScrollHitBottom();
public void onScrollHitTop();
public void onScrollInBetween();
}
private ScrollEndListener listener;
public EndDetectScrollView(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
public EndDetectScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
public EndDetectScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
public void setScrollEndListener(ScrollEndListener l) {
listener = l;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
View bottomChild = (View) getChildAt(getChildCount() - 1);
int bottomDiff = (bottomChild.getBottom() - (getHeight() + getScrollY() + bottomChild
.getTop()));// Calculate the scrolldiff
if (getScrollY() == 0) { // if scrollY==0 top has been reached
if (listener != null)
listener.onScrollHitTop();
mCurrState = ScrollState.TOP;
} else if (bottomDiff == 0) {
if (listener != null)
listener.onScrollHitBottom();
mCurrState = ScrollState.BOTTOM;
} else {
if (listener != null)
listener.onScrollInBetween();
mCurrState = ScrollState.BETWEEN;
}
super.onScrollChanged(l, t, oldl, oldt);
}
public boolean isAtTop() {
return mCurrState == ScrollState.TOP;
}
public boolean isAtBottom() {
return mCurrState == ScrollState.BOTTOM;
}
public boolean isInBetween() {
return mCurrState == ScrollState.BETWEEN;
}
public void setScrollingEnabled(boolean scrollable) {
this.scrollable = scrollable;
}
public boolean isScrollable() {
return scrollable;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// if we can scroll pass the event to the superclass
if (scrollable)
return super.onTouchEvent(ev);
// only continue to handle the touch event if scrolling enabled
return scrollable; // scrollable is always false at this point
default:
return super.onTouchEvent(ev);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Don't do anything with intercepted touch events if
// we are not scrollable
if (!scrollable)
return false;
else
return super.onInterceptTouchEvent(ev);
}
}
package com.mrane.navigation;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import app.insti.R;
public class SlidingUpPanelLayout extends ViewGroup {
/**
* Default peeking out panel height
*/
private static final int DEFAULT_PANEL_HEIGHT = 68; // dp;
/**
* Default anchor point height
*/
private static final float DEFAULT_ANCHOR_POINT = 1.0f; // In relative %
/**
* Default initial state for the component
*/
private static SlideState DEFAULT_SLIDE_STATE = SlideState.COLLAPSED;
/**
* Default height of the shadow above the peeking out panel
*/
private static final int DEFAULT_SHADOW_HEIGHT = 4; // dp;
/**
* If no fade color is given by default it will fade to 80% gray.
*/
private static final int DEFAULT_FADE_COLOR = 0x99000000;
/**
* Default Minimum velocity that will be detected as a fling
*/
private static final int DEFAULT_MIN_FLING_VELOCITY = 400; // dips per second
/**
* Default is set to false because that is how it was written
*/
private static final boolean DEFAULT_OVERLAY_FLAG = false;
/**
* Default attributes for layout
*/
private static final int[] DEFAULT_ATTRS = new int[] {
android.R.attr.gravity
};
/**
* Minimum velocity that will be detected as a fling
*/
private int mMinFlingVelocity = DEFAULT_MIN_FLING_VELOCITY;
/**
* The fade color used for the panel covered by the slider. 0 = no fading.
*/
private int mCoveredFadeColor = DEFAULT_FADE_COLOR;
/**
* Default paralax length of the main view
*/
private static final int DEFAULT_PARALAX_OFFSET = 0;
/**
* The paint used to dim the main layout when sliding
*/
private final Paint mCoveredFadePaint = new Paint();
/**
* Drawable used to draw the shadow between panes.
*/
private final Drawable mShadowDrawable;
/**
* The size of the overhang in pixels.
*/
private int mPanelHeight = -1;
/**
* The size of the shadow in pixels.
*/
private int mShadowHeight = -1;
/**
* Paralax offset
*/
private int mParallaxOffset = -1;
/**
* True if the collapsed panel should be dragged up.
*/
private boolean mIsSlidingUp;
/**
* Panel overlays the windows instead of putting it underneath it.
*/
private boolean mOverlayContent = DEFAULT_OVERLAY_FLAG;
/**
* If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be
* used for dragging.
*/
private View mDragView;
/**
* If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be
* used for dragging.
*/
private int mDragViewResId = -1;
/**
* The child view that can slide, if any.
*/
private View mSlideableView;
/**
* The main view
*/
private View mMainView;
/**
* Current state of the slideable view.
*/
private enum SlideState {
EXPANDED,
COLLAPSED,
ANCHORED,
HIDDEN,
DRAGGING
}
private SlideState mSlideState = SlideState.COLLAPSED;
/**
* How far the panel is offset from its expanded position.
* range [0, 1] where 0 = collapsed, 1 = expanded.
*/
private float mSlideOffset;
/**
* How far in pixels the slideable panel may move.
*/
private int mSlideRange;
/**
* A panel view is locked into internal scrolling or another condition that
* is preventing a drag.
*/
private boolean mIsUnableToDrag;
/**
* Flag indicating that sliding feature is enabled\disabled
*/
private boolean mIsSlidingEnabled;
/**
* Flag indicating if a drag view can have its own touch events. If set
* to true, a drag view can scroll horizontally and have its own click listener.
*
* Default is set to false.
*/
private boolean mIsUsingDragViewTouchEvents;
private float mInitialMotionX;
private float mInitialMotionY;
private float mAnchorPoint = 1.f;
private PanelSlideListener mPanelSlideListener;
private final ViewDragHelper mDragHelper;
/**
* Stores whether or not the pane was expanded the last time it was slideable.
* If expand/collapse operations are invoked this state is modified. Used by
* instance state save/restore.
*/
private boolean mFirstLayout = true;
private final Rect mTmpRect = new Rect();
/**
* Listener for monitoring events about sliding panes.
*/
public interface PanelSlideListener {
/**
* Called when a sliding pane's position changes.
* @param panel The child view that was moved
* @param slideOffset The new offset of this sliding pane within its range, from 0-1
*/
public void onPanelSlide(View panel, float slideOffset);
/**
* Called when a sliding panel becomes slid completely collapsed.
* @param panel The child view that was slid to an collapsed position
*/
public void onPanelCollapsed(View panel);
/**
* Called when a sliding panel becomes slid completely expanded.
* @param panel The child view that was slid to a expanded position
*/
public void onPanelExpanded(View panel);
/**
* Called when a sliding panel becomes anchored.
* @param panel The child view that was slid to a anchored position
*/
public void onPanelAnchored(View panel);
/**
* Called when a sliding panel becomes completely hidden.
* @param panel The child view that was slid to a hidden position
*/
public void onPanelHidden(View panel);
}
public SlidingUpPanelLayout(Context context) {
this(context, null);
}
public SlidingUpPanelLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlidingUpPanelLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if(isInEditMode()) {
mShadowDrawable = null;
mDragHelper = null;
return;
}
if (attrs != null) {
TypedArray defAttrs = context.obtainStyledAttributes(attrs, DEFAULT_ATTRS);
if (defAttrs != null) {
int gravity = defAttrs.getInt(0, Gravity.NO_GRAVITY);
if (gravity != Gravity.TOP && gravity != Gravity.BOTTOM) {
throw new IllegalArgumentException("gravity must be set to either top or bottom");
}
mIsSlidingUp = gravity == Gravity.BOTTOM;
}
defAttrs.recycle();
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlidingUpPanelLayout);
if (ta != null) {
mPanelHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_panelHeight, -1);
mShadowHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_shadowHeight, -1);
mParallaxOffset = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_paralaxOffset, -1);
mMinFlingVelocity = ta.getInt(R.styleable.SlidingUpPanelLayout_flingVelocity, DEFAULT_MIN_FLING_VELOCITY);
mCoveredFadeColor = ta.getColor(R.styleable.SlidingUpPanelLayout_fadeColor, DEFAULT_FADE_COLOR);
mDragViewResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_dragView, -1);
mOverlayContent = ta.getBoolean(R.styleable.SlidingUpPanelLayout_overlay,DEFAULT_OVERLAY_FLAG);
mAnchorPoint = ta.getFloat(R.styleable.SlidingUpPanelLayout_anchorPoint, DEFAULT_ANCHOR_POINT);
mSlideState = SlideState.values()[ta.getInt(R.styleable.SlidingUpPanelLayout_initialState, DEFAULT_SLIDE_STATE.ordinal())];
}
ta.recycle();
}
final float density = context.getResources().getDisplayMetrics().density;
if (mPanelHeight == -1) {
mPanelHeight = (int) (DEFAULT_PANEL_HEIGHT * density + 0.5f);
}
if (mShadowHeight == -1) {
mShadowHeight = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f);
}
if (mParallaxOffset == -1) {
mParallaxOffset = (int) (DEFAULT_PARALAX_OFFSET * density);
}
// If the shadow height is zero, don't show the shadow
if (mShadowHeight > 0) {
if (mIsSlidingUp) {
mShadowDrawable = getResources().getDrawable(R.drawable.above_shadow);
} else {
mShadowDrawable = getResources().getDrawable(R.drawable.below_shadow);
}
} else {
mShadowDrawable = null;
}
setWillNotDraw(false);
mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback());
mDragHelper.setMinVelocity(mMinFlingVelocity * density);
mIsSlidingEnabled = true;
}
/**
* Set the Drag View after the view is inflated
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (mDragViewResId != -1) {
setDragView(findViewById(mDragViewResId));
}
}
public boolean isSlidingEnabled() {
return mIsSlidingEnabled && mSlideableView != null;
}
/**
* Set the collapsed panel height in pixels
*
* @param val A height in pixels
*/
public void setPanelHeight(int val) {
mPanelHeight = val;
int newTop = computePanelTopPosition(0.0f);
mSlideOffset = computeSlideOffset(newTop);
onPanelDragged(newTop);
}
/**
* @return The current collapsed panel height
*/
public int getPanelHeight() {
return mPanelHeight;
}
/**
* @return The current paralax offset
*/
public int getCurrentParalaxOffset() {
// Clamp slide offset at zero for parallax computation;
int offset = (int)(mParallaxOffset * Math.max(mSlideOffset, 0));
return mIsSlidingUp ? -offset : offset;
}
/**
* Sets the panel slide listener
* @param listener
*/
public void setPanelSlideListener(PanelSlideListener listener) {
mPanelSlideListener = listener;
}
/**
* Set the draggable view portion. Use to null, to allow the whole panel to be draggable
*
* @param dragView A view that will be used to drag the panel.
*/
public void setDragView(View dragView) {
if (mDragView != null) {
mDragView.setOnClickListener(null);
}
mDragView = dragView;
if (mDragView != null) {
mDragView.setClickable(true);
mDragView.setFocusable(false);
mDragView.setFocusableInTouchMode(false);
mDragView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (!isEnabled()) return;
if (!isPanelExpanded() && !isPanelAnchored()) {
expandPanel(mAnchorPoint);
} else {
collapsePanel();
}
}
});;
}
}
/**
* Set an anchor point where the panel can stop during sliding
*
* @param anchorPoint A value between 0 and 1, determining the position of the anchor point
* starting from the top of the layout.
*/
public void setAnchorPoint(float anchorPoint) {
if (anchorPoint > 0 && anchorPoint <= 1) {
mAnchorPoint = anchorPoint;
}
}
void dispatchOnPanelSlide(View panel) {
if (mPanelSlideListener != null) {
mPanelSlideListener.onPanelSlide(panel, mSlideOffset);
}
}
void dispatchOnPanelExpanded(View panel) {
if (mPanelSlideListener != null) {
mPanelSlideListener.onPanelExpanded(panel);
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
void dispatchOnPanelCollapsed(View panel) {
if (mPanelSlideListener != null) {
mPanelSlideListener.onPanelCollapsed(panel);
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
void dispatchOnPanelAnchored(View panel) {
if (mPanelSlideListener != null) {
mPanelSlideListener.onPanelAnchored(panel);
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
void dispatchOnPanelHidden(View panel) {
if (mPanelSlideListener != null) {
mPanelSlideListener.onPanelHidden(panel);
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
void updateObscuredViewVisibility() {
if (getChildCount() == 0) {
return;
}
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - getPaddingRight();
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - getPaddingBottom();
final int left;
final int right;
final int top;
final int bottom;
if (mSlideableView != null && hasOpaqueBackground(mSlideableView)) {
left = mSlideableView.getLeft();
right = mSlideableView.getRight();
top = mSlideableView.getTop();
bottom = mSlideableView.getBottom();
} else {
left = right = top = bottom = 0;
}
View child = getChildAt(0);
final int clampedChildLeft = Math.max(leftBound, child.getLeft());
final int clampedChildTop = Math.max(topBound, child.getTop());
final int clampedChildRight = Math.min(rightBound, child.getRight());
final int clampedChildBottom = Math.min(bottomBound, child.getBottom());
final int vis;
if (clampedChildLeft >= left && clampedChildTop >= top &&
clampedChildRight <= right && clampedChildBottom <= bottom) {
vis = INVISIBLE;
} else {
vis = VISIBLE;
}
child.setVisibility(vis);
}
void setAllChildrenVisible() {
for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == INVISIBLE) {
child.setVisibility(VISIBLE);
}
}
}
private static boolean hasOpaqueBackground(View v) {
final Drawable bg = v.getBackground();
return bg != null && bg.getOpacity() == PixelFormat.OPAQUE;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mFirstLayout = true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
} else if (heightMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Height must have an exact value or MATCH_PARENT");
}
final int childCount = getChildCount();
if (childCount != 2) {
throw new IllegalStateException("Sliding up panel layout must have exactly 2 children!");
}
mMainView = getChildAt(0);
mSlideableView = getChildAt(1);
if (mDragView == null) {
setDragView(mSlideableView);
}
// If the sliding panel is not visible, then put the whole view in the hidden state
if (mSlideableView.getVisibility() == GONE) {
mSlideState = SlideState.HIDDEN;
}
int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
// First pass. Measure based on child LayoutParams width/height.
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// We always measure the sliding panel in order to know it's height (needed for show panel)
if (child.getVisibility() == GONE && i == 0) {
continue;
}
int height = layoutHeight;
if (child == mMainView && !mOverlayContent && mSlideState != SlideState.HIDDEN) {
height -= mPanelHeight;
}
int childWidthSpec;
if (lp.width == LayoutParams.WRAP_CONTENT) {
childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
} else if (lp.width == LayoutParams.MATCH_PARENT) {
childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
} else {
childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
}
int childHeightSpec;
if (lp.height == LayoutParams.WRAP_CONTENT) {
childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
} else if (lp.height == LayoutParams.MATCH_PARENT) {
childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
}
if (child == mSlideableView) {
mSlideRange = MeasureSpec.getSize(childHeightSpec) - mPanelHeight;
}
child.measure(childWidthSpec, childHeightSpec);
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
final int childCount = getChildCount();
if (mFirstLayout) {
switch (mSlideState) {
case EXPANDED:
mSlideOffset = 1.0f;
break;
case ANCHORED:
mSlideOffset = mAnchorPoint;
break;
case HIDDEN:
int newTop = computePanelTopPosition(0.0f) + (mIsSlidingUp ? +mPanelHeight : -mPanelHeight);
mSlideOffset = computeSlideOffset(newTop);
break;
default:
mSlideOffset = 0.f;
break;
}
}
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
// Always layout the sliding view on the first layout
if (child.getVisibility() == GONE && (i == 0 || mFirstLayout)) {
continue;
}
final int childHeight = child.getMeasuredHeight();
int childTop = paddingTop;
if (child == mSlideableView) {
childTop = computePanelTopPosition(mSlideOffset);
}
if (!mIsSlidingUp) {
if (child == mMainView && !mOverlayContent) {
childTop = computePanelTopPosition(mSlideOffset) + mSlideableView.getMeasuredHeight();
}
}
final int childBottom = childTop + childHeight;
final int childLeft = paddingLeft;
final int childRight = childLeft + child.getMeasuredWidth();
child.layout(childLeft, childTop, childRight, childBottom);
}
if (mFirstLayout) {
updateObscuredViewVisibility();
}
mFirstLayout = false;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Recalculate sliding panes and their details
if (h != oldh) {
mFirstLayout = true;
}
}
@Override
public void setEnabled(boolean enabled) {
if (!enabled) {
collapsePanel();
}
super.setEnabled(enabled);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (!isEnabled() || !mIsSlidingEnabled || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) {
mDragHelper.cancel();
return super.onInterceptTouchEvent(ev);
}
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mDragHelper.cancel();
return false;
}
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN: {
mIsUnableToDrag = false;
mInitialMotionX = x;
mInitialMotionY = y;
break;
}
case MotionEvent.ACTION_MOVE: {
final float adx = Math.abs(x - mInitialMotionX);
final float ady = Math.abs(y - mInitialMotionY);
final int dragSlop = mDragHelper.getTouchSlop();
// Handle any horizontal scrolling on the drag view.
if (mIsUsingDragViewTouchEvents && adx > dragSlop && ady < dragSlop) {
return super.onInterceptTouchEvent(ev);
}
if ((ady > dragSlop && adx > ady) || !isDragViewUnder((int)mInitialMotionX, (int)mInitialMotionY)) {
mDragHelper.cancel();
mIsUnableToDrag = true;
return false;
}
break;
}
}
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isSlidingEnabled()) {
return super.onTouchEvent(ev);
}
mDragHelper.processTouchEvent(ev);
return true;
}
private boolean isDragViewUnder(int x, int y) {
if (mDragView == null) return false;
int[] viewLocation = new int[2];
mDragView.getLocationOnScreen(viewLocation);
int[] parentLocation = new int[2];
this.getLocationOnScreen(parentLocation);
int screenX = parentLocation[0] + x;
int screenY = parentLocation[1] + y;
return screenX >= viewLocation[0] && screenX < viewLocation[0] + mDragView.getWidth() &&
screenY >= viewLocation[1] && screenY < viewLocation[1] + mDragView.getHeight();
}
private boolean expandPanel(View pane, int initialVelocity, float mSlideOffset) {
return mFirstLayout || smoothSlideTo(mSlideOffset, initialVelocity);
}
private boolean collapsePanel(View pane, int initialVelocity) {
return mFirstLayout || smoothSlideTo(0.0f, initialVelocity);
}
/*
* Computes the top position of the panel based on the slide offset.
*/
private int computePanelTopPosition(float slideOffset) {
int slidingViewHeight = mSlideableView != null ? mSlideableView.getMeasuredHeight() : 0;
int slidePixelOffset = (int) (slideOffset * mSlideRange);
// Compute the top of the panel if its collapsed
return mIsSlidingUp
? getMeasuredHeight() - getPaddingBottom() - mPanelHeight - slidePixelOffset
: getPaddingTop() - slidingViewHeight + mPanelHeight + slidePixelOffset;
}
/*
* Computes the slide offset based on the top position of the panel
*/
private float computeSlideOffset(int topPosition) {
// Compute the panel top position if the panel is collapsed (offset 0)
final int topBoundCollapsed = computePanelTopPosition(0);
// Determine the new slide offset based on the collapsed top position and the new required
// top position
return (mIsSlidingUp
? (float) (topBoundCollapsed - topPosition) / mSlideRange
: (float) (topPosition - topBoundCollapsed) / mSlideRange);
}
/**
* Collapse the sliding pane if it is currently slideable. If first layout
* has already completed this will animate.
*
* @return true if the pane was slideable and is now collapsed/in the process of collapsing
*/
public boolean collapsePanel() {
if (mFirstLayout) {
mSlideState = SlideState.COLLAPSED;
return true;
} else {
if (mSlideState == SlideState.HIDDEN || mSlideState == SlideState.COLLAPSED)
return false;
return collapsePanel(mSlideableView, 0);
}
}
/**
* Partially expand the sliding panel up to a specific offset
*
* @param mSlideOffset Value between 0 and 1, where 0 is completely expanded.
* @return true if the pane was slideable and is now expanded/in the process of expanding
*/
public boolean expandPanel(float mSlideOffset) {
if (mSlideableView == null || mSlideState == SlideState.EXPANDED) return false;
mSlideableView.setVisibility(View.VISIBLE);
return expandPanel(mSlideableView, 0, mSlideOffset);
}
/**
* Check if the sliding panel in this layout is fully expanded.
*
* @return true if sliding panel is completely expanded
*/
public boolean isPanelExpanded() {
return mSlideState == SlideState.EXPANDED;
}
/**
* Check if the sliding panel in this layout is anchored.
*
* @return true if sliding panel is anchored
*/
public boolean isPanelAnchored() {
return mSlideState == SlideState.ANCHORED;
}
@SuppressLint("NewApi")
private void onPanelDragged(int newTop) {
mSlideState = SlideState.DRAGGING;
// Recompute the slide offset based on the new top position
mSlideOffset = computeSlideOffset(newTop);
// Update the parallax based on the new slide offset
if (mParallaxOffset > 0 && mSlideOffset >= 0) {
int mainViewOffset = getCurrentParalaxOffset();
mMainView.setTranslationY(mainViewOffset);
}
// Dispatch the slide event
dispatchOnPanelSlide(mSlideableView);
// If the slide offset is negative, and overlay is not on, we need to increase the
// height of the main content
if (mSlideOffset <= 0 && !mOverlayContent) {
// expand the main view
LayoutParams lp = (LayoutParams)mMainView.getLayoutParams();
lp.height = mIsSlidingUp ? (newTop - getPaddingBottom()) : (getHeight() - getPaddingBottom() - mSlideableView.getMeasuredHeight() - newTop);
mMainView.requestLayout();
}
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
boolean result;
final int save = canvas.save(Canvas.CLIP_SAVE_FLAG);
if (isSlidingEnabled() && mSlideableView != child) {
// Clip against the slider; no sense drawing what will immediately be covered,
// Unless the panel is set to overlay content
if (!mOverlayContent) {
canvas.getClipBounds(mTmpRect);
if (mIsSlidingUp) {
mTmpRect.bottom = Math.min(mTmpRect.bottom, mSlideableView.getTop());
} else {
mTmpRect.top = Math.max(mTmpRect.top, mSlideableView.getBottom());
}
canvas.clipRect(mTmpRect);
}
}
result = super.drawChild(canvas, child, drawingTime);
canvas.restoreToCount(save);
if (mCoveredFadeColor != 0 && mSlideOffset > 0) {
final int baseAlpha = (mCoveredFadeColor & 0xff000000) >>> 24;
final int imag = (int) (baseAlpha * mSlideOffset);
final int color = imag << 24 | (mCoveredFadeColor & 0xffffff);
mCoveredFadePaint.setColor(color);
canvas.drawRect(mTmpRect, mCoveredFadePaint);
}
return result;
}
/**
* Smoothly animate mDraggingPane to the target X position within its range.
*
* @param slideOffset position to animate to
* @param velocity initial velocity in case of fling, or 0.
*/
boolean smoothSlideTo(float slideOffset, int velocity) {
if (!isSlidingEnabled()) {
// Nothing to do.
return false;
}
int panelTop = computePanelTopPosition(slideOffset);
if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), panelTop)) {
setAllChildrenVisible();
ViewCompat.postInvalidateOnAnimation(this);
return true;
}
return false;
}
@Override
public void computeScroll() {
if (mDragHelper != null && mDragHelper.continueSettling(true)) {
if (!isSlidingEnabled()) {
mDragHelper.abort();
return;
}
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
public void draw(Canvas c) {
super.draw(c);
if (!isSlidingEnabled()) {
// No need to draw a shadow if we don't have one.
return;
}
final int right = mSlideableView.getRight();
final int top;
final int bottom;
if (mIsSlidingUp) {
top = mSlideableView.getTop() - mShadowHeight;
bottom = mSlideableView.getTop();
} else {
top = mSlideableView.getBottom();
bottom = mSlideableView.getBottom() + mShadowHeight;
}
final int left = mSlideableView.getLeft();
if (mShadowDrawable != null) {
mShadowDrawable.setBounds(left, top, right, bottom);
mShadowDrawable.draw(c);
}
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams();
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof MarginLayoutParams
? new LayoutParams((MarginLayoutParams) p)
: new LayoutParams(p);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams && super.checkLayoutParams(p);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.mSlideState = mSlideState;
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mSlideState = ss.mSlideState;
}
private class DragHelperCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
if (mIsUnableToDrag) {
return false;
}
return child == mSlideableView;
}
@Override
public void onViewDragStateChanged(int state) {
if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
mSlideOffset = computeSlideOffset(mSlideableView.getTop());
if (mSlideOffset == 1) {
if (mSlideState != SlideState.EXPANDED) {
updateObscuredViewVisibility();
mSlideState = SlideState.EXPANDED;
dispatchOnPanelExpanded(mSlideableView);
}
} else if (mSlideOffset == 0) {
if (mSlideState != SlideState.COLLAPSED) {
mSlideState = SlideState.COLLAPSED;
dispatchOnPanelCollapsed(mSlideableView);
}
} else if (mSlideOffset < 0) {
mSlideState = SlideState.HIDDEN;
mSlideableView.setVisibility(View.GONE);
dispatchOnPanelHidden(mSlideableView);
} else if (mSlideState != SlideState.ANCHORED) {
updateObscuredViewVisibility();
mSlideState = SlideState.ANCHORED;
dispatchOnPanelAnchored(mSlideableView);
}
}
}
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
setAllChildrenVisible();
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
onPanelDragged(top);
invalidate();
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
int target = 0;
// direction is always positive if we are sliding in the expanded direction
float direction = mIsSlidingUp ? -yvel : yvel;
if (direction > 0) {
// swipe up -> expand
target = computePanelTopPosition(1.0f);
} else if (direction < 0) {
// swipe down -> collapse
target = computePanelTopPosition(0.0f);
} else if (mAnchorPoint != 1 && mSlideOffset >= (1.f + mAnchorPoint) / 2) {
// zero velocity, and far enough from anchor point => expand to the top
target = computePanelTopPosition(1.0f);
} else if (mAnchorPoint == 1 && mSlideOffset >= 0.5f) {
// zero velocity, and far enough from anchor point => expand to the top
target = computePanelTopPosition(1.0f);
} else if (mAnchorPoint != 1 && mSlideOffset >= mAnchorPoint) {
target = computePanelTopPosition(mAnchorPoint);
} else if (mAnchorPoint != 1 && mSlideOffset >= mAnchorPoint / 2) {
target = computePanelTopPosition(mAnchorPoint);
} else {
// settle at the bottom
target = computePanelTopPosition(0.0f);
}
mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), target);
invalidate();
}
@Override
public int getViewVerticalDragRange(View child) {
return mSlideRange;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int collapsedTop = computePanelTopPosition(0.f);
final int expandedTop = computePanelTopPosition(1.0f);
if (mIsSlidingUp) {
return Math.min(Math.max(top, expandedTop), collapsedTop);
} else {
return Math.min(Math.max(top, collapsedTop), expandedTop);
}
}
}
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
private static final int[] ATTRS = new int[] {
android.R.attr.layout_weight
};
public LayoutParams() {
super(MATCH_PARENT, MATCH_PARENT);
}
public LayoutParams(android.view.ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS);
a.recycle();
}
}
static class SavedState extends BaseSavedState {
SlideState mSlideState;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
try {
mSlideState = Enum.valueOf(SlideState.class, in.readString());
} catch (IllegalArgumentException e) {
mSlideState = SlideState.COLLAPSED;
}
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeString(mSlideState.toString());
}
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mrane.navigation;
import android.content.Context;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.widget.ScrollerCompat;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import java.util.Arrays;
/**
* ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
* of useful operations and state tracking for allowing a user to drag and reposition
* views within their parent ViewGroup.
*/
public class ViewDragHelper {
/**
* A null/invalid pointer ID.
*/
public static final int INVALID_POINTER = -1;
/**
* A view is not currently being dragged or animating as a result of a fling/snap.
*/
public static final int STATE_IDLE = 0;
/**
* A view is currently being dragged. The position is currently changing as a result
* of user input or simulated user input.
*/
public static final int STATE_DRAGGING = 1;
/**
* A view is currently settling into place as a result of a fling or
* predefined non-interactive motion.
*/
public static final int STATE_SETTLING = 2;
/**
* Edge flag indicating that the left edge should be affected.
*/
public static final int EDGE_LEFT = 1 << 0;
/**
* Edge flag indicating that the right edge should be affected.
*/
public static final int EDGE_RIGHT = 1 << 1;
/**
* Edge flag indicating that the top edge should be affected.
*/
public static final int EDGE_TOP = 1 << 2;
/**
* Edge flag indicating that the bottom edge should be affected.
*/
public static final int EDGE_BOTTOM = 1 << 3;
/**
* Indicates that a check should occur along the horizontal axis
*/
public static final int DIRECTION_HORIZONTAL = 1 << 0;
/**
* Indicates that a check should occur along the vertical axis
*/
public static final int DIRECTION_VERTICAL = 1 << 1;
private static final int EDGE_SIZE = 20; // dp
private static final int BASE_SETTLE_DURATION = 256; // ms
private static final int MAX_SETTLE_DURATION = 600; // ms
// Current drag state; idle, dragging or settling
private int mDragState;
// Distance to travel before a drag may begin
private int mTouchSlop;
// Last known position/pointer tracking
private int mActivePointerId = INVALID_POINTER;
private float[] mInitialMotionX;
private float[] mInitialMotionY;
private float[] mLastMotionX;
private float[] mLastMotionY;
private int[] mInitialEdgesTouched;
private int[] mEdgeDragsInProgress;
private int[] mEdgeDragsLocked;
private int mPointersDown;
private VelocityTracker mVelocityTracker;
private float mMaxVelocity;
private float mMinVelocity;
private int mEdgeSize;
private int mTrackingEdges;
private ScrollerCompat mScroller;
private final Callback mCallback;
private View mCapturedView;
private boolean mReleaseInProgress;
private final ViewGroup mParentView;
/**
* A Callback is used as a communication channel with the ViewDragHelper back to the
* parent view using it. <code>on*</code>methods are invoked on siginficant events and several
* accessor methods are expected to provide the ViewDragHelper with more information
* about the state of the parent view upon request. The callback also makes decisions
* governing the range and draggability of child views.
*/
public static abstract class Callback {
/**
* Called when the drag state changes. See the <code>STATE_*</code> constants
* for more information.
*
* @param state The new drag state
*
* @see #STATE_IDLE
* @see #STATE_DRAGGING
* @see #STATE_SETTLING
*/
public void onViewDragStateChanged(int state) {}
/**
* Called when the captured view's position changes as the result of a drag or settle.
*
* @param changedView View whose position changed
* @param left New X coordinate of the left edge of the view
* @param top New Y coordinate of the top edge of the view
* @param dx Change in X position from the last call
* @param dy Change in Y position from the last call
*/
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
/**
* Called when a child view is captured for dragging or settling. The ID of the pointer
* currently dragging the captured view is supplied. If activePointerId is
* identified as {@link #INVALID_POINTER} the capture is programmatic instead of
* pointer-initiated.
*
* @param capturedChild Child view that was captured
* @param activePointerId Pointer id tracking the child capture
*/
public void onViewCaptured(View capturedChild, int activePointerId) {}
/**
* Called when the child view is no longer being actively dragged.
* The fling velocity is also supplied, if relevant. The velocity values may
* be clamped to system minimums or maximums.
*
* <p>Calling code may decide to fling or otherwise release the view to let it
* settle into place. It should do so using {@link #settleCapturedViewAt(int, int)}
* or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes
* one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING}
* and the view capture will not fully end until it comes to a complete stop.
* If neither of these methods is invoked before <code>onViewReleased</code> returns,
* the view will stop in place and the ViewDragHelper will return to
* {@link #STATE_IDLE}.</p>
*
* @param releasedChild The captured child view now being released
* @param xvel X velocity of the pointer as it left the screen in pixels per second.
* @param yvel Y velocity of the pointer as it left the screen in pixels per second.
*/
public void onViewReleased(View releasedChild, float xvel, float yvel) {}
/**
* Called when one of the subscribed edges in the parent view has been touched
* by the user while no child view is currently captured.
*
* @param edgeFlags A combination of edge flags describing the edge(s) currently touched
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void onEdgeTouched(int edgeFlags, int pointerId) {}
/**
* Called when the given edge may become locked. This can happen if an edge drag
* was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)}
* was called. This method should return true to lock this edge or false to leave it
* unlocked. The default behavior is to leave edges unlocked.
*
* @param edgeFlags A combination of edge flags describing the edge(s) locked
* @return true to lock the edge, false to leave it unlocked
*/
public boolean onEdgeLock(int edgeFlags) {
return false;
}
/**
* Called when the user has started a deliberate drag away from one
* of the subscribed edges in the parent view while no child view is currently captured.
*
* @param edgeFlags A combination of edge flags describing the edge(s) dragged
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
/**
* Called to determine the Z-order of child views.
*
* @param index the ordered position to query for
* @return index of the view that should be ordered at position <code>index</code>
*/
public int getOrderedChildIndex(int index) {
return index;
}
/**
* Return the magnitude of a draggable child view's horizontal range of motion in pixels.
* This method should return 0 for views that cannot move horizontally.
*
* @param child Child view to check
* @return range of horizontal motion in pixels
*/
public int getViewHorizontalDragRange(View child) {
return 0;
}
/**
* Return the magnitude of a draggable child view's vertical range of motion in pixels.
* This method should return 0 for views that cannot move vertically.
*
* @param child Child view to check
* @return range of vertical motion in pixels
*/
public int getViewVerticalDragRange(View child) {
return 0;
}
/**
* Called when the user's input indicates that they want to capture the given child view
* with the pointer indicated by pointerId. The callback should return true if the user
* is permitted to drag the given view with the indicated pointer.
*
* <p>ViewDragHelper may call this method multiple times for the same view even if
* the view is already captured; this indicates that a new pointer is trying to take
* control of the view.</p>
*
* <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)}
* will follow if the capture is successful.</p>
*
* @param child Child the user is attempting to capture
* @param pointerId ID of the pointer attempting the capture
* @return true if capture should be allowed, false otherwise
*/
public abstract boolean tryCaptureView(View child, int pointerId);
/**
* Restrict the motion of the dragged child view along the horizontal axis.
* The default implementation does not allow horizontal motion; the extending
* class must override this method and provide the desired clamping.
*
*
* @param child Child view being dragged
* @param left Attempted motion along the X axis
* @param dx Proposed change in position for left
* @return The new clamped position for left
*/
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
/**
* Restrict the motion of the dragged child view along the vertical axis.
* The default implementation does not allow vertical motion; the extending
* class must override this method and provide the desired clamping.
*
*
* @param child Child view being dragged
* @param top Attempted motion along the Y axis
* @param dy Proposed change in position for top
* @return The new clamped position for top
*/
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
}
/**
* Interpolator defining the animation curve for mScroller
*/
private static final Interpolator sInterpolator = new Interpolator() {
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
private final Runnable mSetIdleRunnable = new Runnable() {
public void run() {
setDragState(STATE_IDLE);
}
};
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
/**
* Apps should use ViewDragHelper.create() to get a new instance.
* This will allow VDH to use internal compatibility implementations for different
* platform versions.
*
* @param context Context to initialize config-dependent params from
* @param forParent Parent view to monitor
*/
private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
if (forParent == null) {
throw new IllegalArgumentException("Parent view may not be null");
}
if (cb == null) {
throw new IllegalArgumentException("Callback may not be null");
}
mParentView = forParent;
mCallback = cb;
final ViewConfiguration vc = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
mTouchSlop = vc.getScaledTouchSlop();
mMaxVelocity = vc.getScaledMaximumFlingVelocity();
mMinVelocity = vc.getScaledMinimumFlingVelocity();
mScroller = ScrollerCompat.create(context, sInterpolator);
}
/**
* Set the minimum velocity that will be detected as having a magnitude greater than zero
* in pixels per second. Callback methods accepting a velocity will be clamped appropriately.
*
* @param minVel Minimum velocity to detect
*/
public void setMinVelocity(float minVel) {
mMinVelocity = minVel;
}
/**
* Retrieve the current drag state of this helper. This will return one of
* {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
* @return The current drag state
*/
public int getViewDragState() {
return mDragState;
}
/**
* Capture a specific child view for dragging within the parent. The callback will be notified
* but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to
* capture this view.
*
* @param childView Child view to capture
* @param activePointerId ID of the pointer that is dragging the captured child view
*/
public void captureChildView(View childView, int activePointerId) {
if (childView.getParent() != mParentView) {
throw new IllegalArgumentException("captureChildView: parameter must be a descendant " +
"of the ViewDragHelper's tracked parent view (" + mParentView + ")");
}
mCapturedView = childView;
mActivePointerId = activePointerId;
mCallback.onViewCaptured(childView, activePointerId);
setDragState(STATE_DRAGGING);
}
/**
* @return The minimum distance in pixels that the user must travel to initiate a drag
*/
public int getTouchSlop() {
return mTouchSlop;
}
/**
* The result of a call to this method is equivalent to
* {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event.
*/
public void cancel() {
mActivePointerId = INVALID_POINTER;
clearMotionHistory();
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
/**
* {@link #cancel()}, but also abort all motion in progress and snap to the end of any
* animation.
*/
public void abort() {
cancel();
if (mDragState == STATE_SETTLING) {
final int oldX = mScroller.getCurrX();
final int oldY = mScroller.getCurrY();
mScroller.abortAnimation();
final int newX = mScroller.getCurrX();
final int newY = mScroller.getCurrY();
mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY);
}
setDragState(STATE_IDLE);
}
/**
* Animate the view <code>child</code> to the given (left, top) position.
* If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*
* <p>This operation does not count as a capture event, though {@link #getCapturedView()}
* will still report the sliding view while the slide is in progress.</p>
*
* @param child Child view to capture and animate
* @param finalLeft Final left position of child
* @param finalTop Final top position of child
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
mCapturedView = child;
mActivePointerId = INVALID_POINTER;
return forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
}
/**
* Settle the captured view at the given (left, top) position.
* The appropriate velocity from prior motion will be taken into account.
* If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*
* @param finalLeft Settled left edge position for the captured view
* @param finalTop Settled top edge position for the captured view
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " +
"Callback#onViewReleased");
}
return forceSettleCapturedViewAt(finalLeft, finalTop,
(int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
(int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId));
}
/**
* Settle the captured view at the given (left, top) position.
*
* @param finalLeft Target left position for the captured view
* @param finalTop Target top position for the captured view
* @param xvel Horizontal velocity
* @param yvel Vertical velocity
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
final int startLeft = mCapturedView.getLeft();
final int startTop = mCapturedView.getTop();
final int dx = finalLeft - startLeft;
final int dy = finalTop - startTop;
if (dx == 0 && dy == 0) {
// Nothing to do. Send callbacks, be done.
mScroller.abortAnimation();
setDragState(STATE_IDLE);
return false;
}
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
setDragState(STATE_SETTLING);
return true;
}
private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {
xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
final int absDx = Math.abs(dx);
final int absDy = Math.abs(dy);
final int absXVel = Math.abs(xvel);
final int absYVel = Math.abs(yvel);
final int addedVel = absXVel + absYVel;
final int addedDistance = absDx + absDy;
final float xweight = xvel != 0 ? (float) absXVel / addedVel :
(float) absDx / addedDistance;
final float yweight = yvel != 0 ? (float) absYVel / addedVel :
(float) absDy / addedDistance;
int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));
int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));
return (int) (xduration * xweight + yduration * yweight);
}
private int computeAxisDuration(int delta, int velocity, int motionRange) {
if (delta == 0) {
return 0;
}
final int width = mParentView.getWidth();
final int halfWidth = width / 2;
final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
final float distance = halfWidth + halfWidth *
distanceInfluenceForSnapDuration(distanceRatio);
int duration;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
final float range = (float) Math.abs(delta) / motionRange;
duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
}
return Math.min(duration, MAX_SETTLE_DURATION);
}
/**
* Clamp the magnitude of value for absMin and absMax.
* If the value is below the minimum, it will be clamped to zero.
* If the value is above the maximum, it will be clamped to the maximum.
*
* @param value Value to clamp
* @param absMin Absolute value of the minimum significant value to return
* @param absMax Absolute value of the maximum value to return
* @return The clamped value with the same sign as <code>value</code>
*/
private int clampMag(int value, int absMin, int absMax) {
final int absValue = Math.abs(value);
if (absValue < absMin) return 0;
if (absValue > absMax) return value > 0 ? absMax : -absMax;
return value;
}
/**
* Clamp the magnitude of value for absMin and absMax.
* If the value is below the minimum, it will be clamped to zero.
* If the value is above the maximum, it will be clamped to the maximum.
*
* @param value Value to clamp
* @param absMin Absolute value of the minimum significant value to return
* @param absMax Absolute value of the maximum value to return
* @return The clamped value with the same sign as <code>value</code>
*/
private float clampMag(float value, float absMin, float absMax) {
final float absValue = Math.abs(value);
if (absValue < absMin) return 0;
if (absValue > absMax) return value > 0 ? absMax : -absMax;
return value;
}
private float distanceInfluenceForSnapDuration(float f) {
f -= 0.5f; // center the values about 0.
f *= 0.3f * Math.PI / 2.0f;
return (float) Math.sin(f);
}
/**
* Move the captured settling view by the appropriate amount for the current time.
* If <code>continueSettling</code> returns true, the caller should call it again
* on the next frame to continue.
*
* @param deferCallbacks true if state callbacks should be deferred via posted message.
* Set this to true if you are calling this method from
* {@link android.view.View#computeScroll()} or similar methods
* invoked as part of layout or drawing.
* @return true if settle is still in progress
*/
public boolean continueSettling(boolean deferCallbacks) {
if (mDragState == STATE_SETTLING) {
boolean keepGoing = mScroller.computeScrollOffset();
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
final int dx = x - mCapturedView.getLeft();
final int dy = y - mCapturedView.getTop();
if (dx != 0) {
mCapturedView.offsetLeftAndRight(dx);
}
if (dy != 0) {
mCapturedView.offsetTopAndBottom(dy);
}
if (dx != 0 || dy != 0) {
mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
}
if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
// Close enough. The interpolator/scroller might think we're still moving
// but the user sure doesn't.
mScroller.abortAnimation();
keepGoing = mScroller.isFinished();
}
if (!keepGoing) {
if (deferCallbacks) {
mParentView.post(mSetIdleRunnable);
} else {
setDragState(STATE_IDLE);
}
}
}
return mDragState == STATE_SETTLING;
}
/**
* Like all callback events this must happen on the UI thread, but release
* involves some extra semantics. During a release (mReleaseInProgress)
* is the only time it is valid to call {@link #settleCapturedViewAt(int, int)}
* or {@link #flingCapturedView(int, int, int, int)}.
*/
private void dispatchViewReleased(float xvel, float yvel) {
mReleaseInProgress = true;
mCallback.onViewReleased(mCapturedView, xvel, yvel);
mReleaseInProgress = false;
if (mDragState == STATE_DRAGGING) {
// onViewReleased didn't call a method that would have changed this. Go idle.
setDragState(STATE_IDLE);
}
}
private void clearMotionHistory() {
if (mInitialMotionX == null) {
return;
}
Arrays.fill(mInitialMotionX, 0);
Arrays.fill(mInitialMotionY, 0);
Arrays.fill(mLastMotionX, 0);
Arrays.fill(mLastMotionY, 0);
Arrays.fill(mInitialEdgesTouched, 0);
Arrays.fill(mEdgeDragsInProgress, 0);
Arrays.fill(mEdgeDragsLocked, 0);
mPointersDown = 0;
}
private void clearMotionHistory(int pointerId) {
if (mInitialMotionX == null) {
return;
}
mInitialMotionX[pointerId] = 0;
mInitialMotionY[pointerId] = 0;
mLastMotionX[pointerId] = 0;
mLastMotionY[pointerId] = 0;
mInitialEdgesTouched[pointerId] = 0;
mEdgeDragsInProgress[pointerId] = 0;
mEdgeDragsLocked[pointerId] = 0;
mPointersDown &= ~(1 << pointerId);
}
private void ensureMotionHistorySizeForId(int pointerId) {
if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) {
float[] imx = new float[pointerId + 1];
float[] imy = new float[pointerId + 1];
float[] lmx = new float[pointerId + 1];
float[] lmy = new float[pointerId + 1];
int[] iit = new int[pointerId + 1];
int[] edip = new int[pointerId + 1];
int[] edl = new int[pointerId + 1];
if (mInitialMotionX != null) {
System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length);
System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length);
System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length);
System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length);
System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length);
System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length);
System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length);
}
mInitialMotionX = imx;
mInitialMotionY = imy;
mLastMotionX = lmx;
mLastMotionY = lmy;
mInitialEdgesTouched = iit;
mEdgeDragsInProgress = edip;
mEdgeDragsLocked = edl;
}
}
private void saveInitialMotion(float x, float y, int pointerId) {
ensureMotionHistorySizeForId(pointerId);
mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
mPointersDown |= 1 << pointerId;
}
private void saveLastMotion(MotionEvent ev) {
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
mLastMotionX[pointerId] = x;
mLastMotionY[pointerId] = y;
}
}
/**
* Check if the given pointer ID represents a pointer that is currently down (to the best
* of the ViewDragHelper's knowledge).
*
* <p>The state used to report this information is populated by the methods
* {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
* {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not
* been called for all relevant MotionEvents to track, the information reported
* by this method may be stale or incorrect.</p>
*
* @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent
* @return true if the pointer with the given ID is still down
*/
public boolean isPointerDown(int pointerId) {
return (mPointersDown & 1 << pointerId) != 0;
}
void setDragState(int state) {
if (mDragState != state) {
mDragState = state;
mCallback.onViewDragStateChanged(state);
if (state == STATE_IDLE) {
mCapturedView = null;
}
}
}
/**
* Attempt to capture the view with the given pointer ID. The callback will be involved.
* This will put us into the "dragging" state. If we've already captured this view with
* this pointer this method will immediately return true without consulting the callback.
*
* @param toCapture View to capture
* @param pointerId Pointer to capture with
* @return true if capture was successful
*/
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
if (toCapture == mCapturedView && mActivePointerId == pointerId) {
// Already done!
return true;
}
if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
mActivePointerId = pointerId;
captureChildView(toCapture, pointerId);
return true;
}
return false;
}
/**
* Check if this event as provided to the parent view's onInterceptTouchEvent should
* cause the parent to intercept the touch event stream.
*
* @param ev MotionEvent provided to onInterceptTouchEvent
* @return true if the parent view should return true from onInterceptTouchEvent
*/
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
final int actionIndex = MotionEventCompat.getActionIndex(ev);
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn't get
// the whole previous stream.
cancel();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = MotionEventCompat.getPointerId(ev, 0);
saveInitialMotion(x, y, pointerId);
final View toCapture = findTopChildUnder((int) x, (int) y);
// Catch a settling view if possible.
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
tryCaptureViewForDrag(toCapture, pointerId);
}
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
final float x = MotionEventCompat.getX(ev, actionIndex);
final float y = MotionEventCompat.getY(ev, actionIndex);
saveInitialMotion(x, y, pointerId);
// A ViewDragHelper can only manipulate one view at a time.
if (mDragState == STATE_IDLE) {
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
} else if (mDragState == STATE_SETTLING) {
// Catch a settling view if possible.
final View toCapture = findTopChildUnder((int) x, (int) y);
if (toCapture == mCapturedView) {
tryCaptureViewForDrag(toCapture, pointerId);
}
}
break;
}
case MotionEvent.ACTION_MOVE: {
// First to cross a touch slop over a draggable view wins. Also report edge drags.
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount && mInitialMotionX != null && mInitialMotionY != null; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
final View toCapture = findTopChildUnder((int)mInitialMotionX[pointerId], (int)mInitialMotionY[pointerId]);
if (toCapture != null && checkTouchSlop(toCapture, dx, dy) &&
tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
break;
}
case MotionEventCompat.ACTION_POINTER_UP: {
final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
clearMotionHistory(pointerId);
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
cancel();
break;
}
}
return mDragState == STATE_DRAGGING;
}
/**
* Process a touch event received by the parent view. This method will dispatch callback events
* as needed before returning. The parent view's onTouchEvent implementation should call this.
*
* @param ev The touch event received by the parent view
*/
public void processTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
final int actionIndex = MotionEventCompat.getActionIndex(ev);
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn't get
// the whole previous stream.
cancel();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = MotionEventCompat.getPointerId(ev, 0);
final View toCapture = findTopChildUnder((int) x, (int) y);
saveInitialMotion(x, y, pointerId);
// Since the parent is already directly processing this touch event,
// there is no reason to delay for a slop before dragging.
// Start immediately if possible.
tryCaptureViewForDrag(toCapture, pointerId);
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
final float x = MotionEventCompat.getX(ev, actionIndex);
final float y = MotionEventCompat.getY(ev, actionIndex);
saveInitialMotion(x, y, pointerId);
// A ViewDragHelper can only manipulate one view at a time.
if (mDragState == STATE_IDLE) {
// If we're idle we can do anything! Treat it like a normal down event.
final View toCapture = findTopChildUnder((int) x, (int) y);
tryCaptureViewForDrag(toCapture, pointerId);
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
} else if (isCapturedViewUnder((int) x, (int) y)) {
// We're still tracking a captured view. If the same view is under this
// point, we'll swap to controlling it with this pointer instead.
// (This will still work if we're "catching" a settling view.)
tryCaptureViewForDrag(mCapturedView, pointerId);
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, index);
final float y = MotionEventCompat.getY(ev, index);
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
saveLastMotion(ev);
} else {
// Check to see if any pointer is now over a draggable view.
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag.
break;
}
final View toCapture = findTopChildUnder((int) x, (int) y);
if (checkTouchSlop(toCapture, dx, dy) &&
tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
}
break;
}
case MotionEventCompat.ACTION_POINTER_UP: {
final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
// Try to find another pointer that's still holding on to the captured view.
int newActivePointer = INVALID_POINTER;
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int id = MotionEventCompat.getPointerId(ev, i);
if (id == mActivePointerId) {
// This one's going away, skip.
continue;
}
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
if (findTopChildUnder((int) x, (int) y) == mCapturedView &&
tryCaptureViewForDrag(mCapturedView, id)) {
newActivePointer = mActivePointerId;
break;
}
}
if (newActivePointer == INVALID_POINTER) {
// We didn't find another pointer still touching the view, release it.
releaseViewForPointerUp();
}
}
clearMotionHistory(pointerId);
break;
}
case MotionEvent.ACTION_UP: {
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();
}
cancel();
break;
}
case MotionEvent.ACTION_CANCEL: {
if (mDragState == STATE_DRAGGING) {
dispatchViewReleased(0, 0);
}
cancel();
break;
}
}
}
private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
int dragsStarted = 0;
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
dragsStarted |= EDGE_LEFT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
dragsStarted |= EDGE_TOP;
}
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) {
dragsStarted |= EDGE_RIGHT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
dragsStarted |= EDGE_BOTTOM;
}
if (dragsStarted != 0) {
mEdgeDragsInProgress[pointerId] |= dragsStarted;
mCallback.onEdgeDragStarted(dragsStarted, pointerId);
}
}
private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
final float absDelta = Math.abs(delta);
final float absODelta = Math.abs(odelta);
if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 ||
(mEdgeDragsLocked[pointerId] & edge) == edge ||
(mEdgeDragsInProgress[pointerId] & edge) == edge ||
(absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
return false;
}
if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
mEdgeDragsLocked[pointerId] |= edge;
return false;
}
return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
}
/**
* Check if we've crossed a reasonable touch slop for the given child view.
* If the child cannot be dragged along the horizontal or vertical axis, motion
* along that axis will not count toward the slop check.
*
* @param child Child to check
* @param dx Motion since initial position along X axis
* @param dy Motion since initial position along Y axis
* @return true if the touch slop has been crossed
*/
private boolean checkTouchSlop(View child, float dx, float dy) {
if (child == null) {
return false;
}
final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;
if (checkHorizontal && checkVertical) {
return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
} else if (checkHorizontal) {
return Math.abs(dx) > mTouchSlop;
} else if (checkVertical) {
return Math.abs(dy) > mTouchSlop;
}
return false;
}
private void releaseViewForPointerUp() {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final float xvel = clampMag(
VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
mMinVelocity, mMaxVelocity);
final float yvel = clampMag(
VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
mMinVelocity, mMaxVelocity);
dispatchViewReleased(xvel, yvel);
}
private void dragTo(int left, int top, int dx, int dy) {
int clampedX = left;
int clampedY = top;
final int oldLeft = mCapturedView.getLeft();
final int oldTop = mCapturedView.getTop();
if (dx != 0) {
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
}
if (dy != 0) {
clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
mCapturedView.offsetTopAndBottom(clampedY - oldTop);
}
if (dx != 0 || dy != 0) {
final int clampedDx = clampedX - oldLeft;
final int clampedDy = clampedY - oldTop;
mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
clampedDx, clampedDy);
}
}
/**
* Determine if the currently captured view is under the given point in the
* parent view's coordinate system. If there is no captured view this method
* will return false.
*
* @param x X position to test in the parent's coordinate system
* @param y Y position to test in the parent's coordinate system
* @return true if the captured view is under the given point, false otherwise
*/
public boolean isCapturedViewUnder(int x, int y) {
return isViewUnder(mCapturedView, x, y);
}
/**
* Determine if the supplied view is under the given point in the
* parent view's coordinate system.
*
* @param view Child view of the parent to hit test
* @param x X position to test in the parent's coordinate system
* @param y Y position to test in the parent's coordinate system
* @return true if the supplied view is under the given point, false otherwise
*/
public boolean isViewUnder(View view, int x, int y) {
if (view == null) {
return false;
}
return x >= view.getLeft() &&
x < view.getRight() &&
y >= view.getTop() &&
y < view.getBottom();
}
/**
* Find the topmost child under the given point within the parent view's coordinate system.
* The child order is determined using {@link Callback#getOrderedChildIndex(int)}.
*
* @param x X position to test in the parent's coordinate system
* @param y Y position to test in the parent's coordinate system
* @return The topmost child view under (x, y) or null if none found.
*/
public View findTopChildUnder(int x, int y) {
final int childCount = mParentView.getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
if (x >= child.getLeft() && x < child.getRight() &&
y >= child.getTop() && y < child.getBottom()) {
return child;
}
}
return null;
}
private int getEdgesTouched(int x, int y) {
int result = 0;
if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT;
if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP;
if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT;
if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM;
return result;
}
}
\ No newline at end of file
package com.mrane.zoomview;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.BounceInterpolator;
import app.insti.R.drawable;
import app.insti.fragment.MapFragment;
import com.mrane.campusmap.SettingsManager;
import com.mrane.data.Building;
import com.mrane.data.Marker;
import com.mrane.data.Room;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
public class CampusMapView extends SubsamplingScaleImageView {
private MapFragment mainActivity;
private HashMap<String, Marker> data;
private Collection<Marker> markerList;
private ArrayList<Marker> addedMarkerList;
private ArrayList<Marker> specialMarkerList;
private ArrayList<Marker> convoMarkerList;
private Marker resultMarker;
private Bitmap bluePointer;
private Bitmap yellowPointer;
private Bitmap greenPointer;
private Bitmap grayPointer;
private Bitmap blueMarker;
private Bitmap yellowMarker;
private Bitmap greenMarker;
private Bitmap grayMarker;
private Bitmap blueLockedMarker;
private Bitmap blueConvoMarker;
private Bitmap yellowLockedMarker;
private Bitmap greenLockedMarker;
private Bitmap grayLockedMarker;
private float pointerWidth = 12;
private float highlightedMarkerScale;
private Paint paint;
private Paint textPaint;
private Paint strokePaint;
private Rect bounds = new Rect();
private static int RATIO_SHOW_PIN = 10;
private static int RATIO_SHOW_PIN_TEXT = 20;
private static long DURATION_MARKER_ANIMATION = 500;
private static long DELAY_MARKER_ANIMATION = 675;
private static float MAX_SCALE = 1F;
private DisplayMetrics displayMetrics;
private float density;
private boolean isFirstLoad = true;
private SettingsManager settingsManager;
public CampusMapView(Context context) {
this(context, null);
}
public CampusMapView(Context context, AttributeSet attr) {
super(context, attr);
initialise();
}
private void initialise() {
displayMetrics = getResources().getDisplayMetrics();
density = displayMetrics.density;
highlightedMarkerScale = 1.0f;
initMarkers();
initPaints();
mainActivity = MapFragment.getMainActivity();
setGestureDetector();
super.setMaxScale(density * MAX_SCALE);
}
@Override
protected void onImageReady() {
if (isFirstLoad) {
Runnable runnable = new Runnable() {
public void run() {
AnimationBuilder anim;
anim = animateScaleAndCenter(
getTargetMinScale(), MapFragment.MAP_CENTER);
anim.withDuration(MapFragment.DURATION_INIT_MAP_ANIM)
.start();
isFirstLoad = false;
}
};
mainActivity.getActivity().runOnUiThread(runnable);
}
}
public void setSettingsManager(SettingsManager sm){
settingsManager = sm;
}
public void setFirstLoad(boolean b) {
isFirstLoad = b;
}
private void initMarkers() {
float w = 0;
float h = 0;
Options options = new BitmapFactory.Options();
options.inScaled = true;
bluePointer = BitmapFactory.decodeResource(getResources(),
drawable.marker_dot_blue, options);
blueMarker = BitmapFactory.decodeResource(getResources(),
drawable.marker_blue_s, options);
blueLockedMarker = BitmapFactory.decodeResource(getResources(),
drawable.marker_blue_h, options);
blueConvoMarker = BitmapFactory.decodeResource(getResources(),
drawable.marker_blue_h_convo, options);
yellowPointer = BitmapFactory.decodeResource(getResources(),
drawable.marker_dot_yellow, options);
yellowMarker = BitmapFactory.decodeResource(getResources(),
drawable.marker_yellow_s, options);
yellowLockedMarker = BitmapFactory.decodeResource(getResources(),
drawable.marker_yellow_h, options);
greenPointer = BitmapFactory.decodeResource(getResources(),
drawable.marker_dot_green, options);
greenMarker = BitmapFactory.decodeResource(getResources(),
drawable.marker_green_s, options);
greenLockedMarker = BitmapFactory.decodeResource(getResources(),
drawable.marker_green_h, options);
grayPointer = BitmapFactory.decodeResource(getResources(),
drawable.marker_dot_gray, options);
grayMarker = BitmapFactory.decodeResource(getResources(),
drawable.marker_gray_s, options);
grayLockedMarker = BitmapFactory.decodeResource(getResources(),
drawable.marker_gray_h, options);
w = pointerWidth*density;
h = bluePointer.getScaledHeight(displayMetrics) * (w / bluePointer.getScaledWidth(displayMetrics));
bluePointer = Bitmap.createScaledBitmap(bluePointer, (int) w, (int) h,
true);
bluePointer = Bitmap.createScaledBitmap(bluePointer, (int) w, (int) h,
true);
yellowPointer = Bitmap.createScaledBitmap(yellowPointer, (int) w,
(int) h, true);
greenPointer = Bitmap.createScaledBitmap(greenPointer, (int) w,
(int) h, true);
grayPointer = Bitmap.createScaledBitmap(grayPointer, (int) w, (int) h,
true);
w = 4f * w;
h = blueMarker.getScaledHeight(displayMetrics) * (w / blueMarker.getScaledWidth(displayMetrics));
blueMarker = Bitmap.createScaledBitmap(blueMarker, (int) w, (int) h,
true);
yellowMarker = Bitmap.createScaledBitmap(yellowMarker, (int) w,
(int) h, true);
greenMarker = Bitmap.createScaledBitmap(greenMarker, (int) w, (int) h,
true);
grayMarker = Bitmap.createScaledBitmap(grayMarker, (int) w, (int) h,
true);
blueLockedMarker = Bitmap.createScaledBitmap(blueLockedMarker, (int) w,
(int) h, true);
blueConvoMarker = Bitmap.createScaledBitmap(blueConvoMarker, (int) w,
(int) h, true);
yellowLockedMarker = Bitmap.createScaledBitmap(yellowLockedMarker,
(int) w, (int) h, true);
greenLockedMarker = Bitmap.createScaledBitmap(greenLockedMarker,
(int) w, (int) h, true);
grayLockedMarker = Bitmap.createScaledBitmap(grayLockedMarker, (int) w,
(int) h, true);
}
private void initPaints() {
paint = new Paint();
paint.setAntiAlias(true);
textPaint = new Paint();
textPaint.setAntiAlias(true);
// textPaint.setColor(Color.rgb(254, 250, 217));
textPaint.setColor(Color.WHITE);
textPaint.setShadowLayer(8.0f * density, -1 * density, 1 * density,
Color.BLACK);
textPaint.setTextSize(16 * density);
Typeface boldCn = Typeface.createFromAsset(getContext().getAssets(),
MapFragment.FONT_SEMIBOLD);
textPaint.setTypeface(boldCn);
strokePaint = new Paint();
strokePaint.setAntiAlias(true);
strokePaint.setColor(Color.BLACK);
strokePaint.setTypeface(Typeface.DEFAULT_BOLD);
strokePaint.setTextSize(14 * density);
strokePaint.setStyle(Style.STROKE);
strokePaint.setStrokeJoin(Join.ROUND);
strokePaint.setStrokeWidth(0.2f * density);
}
public float getTargetMinScale() {
return Math.max(getWidth() / (float) getSWidth(), (getHeight())
/ (float) getSHeight());
}
public void setData(HashMap<String, Marker> markerData) {
data = markerData;
markerList = data.values();
addedMarkerList = new ArrayList<Marker>();
specialMarkerList = new ArrayList<Marker>();
convoMarkerList = new ArrayList<Marker>();
setSpecialMarkers();
}
private void setSpecialMarkers() {
for (Marker m : markerList) {
if (m.isShowDefault()) {
specialMarkerList.add(m);
}
}
}
public static int getShowPinRatio() {
return RATIO_SHOW_PIN;
}
public static void setShowPinRatio(int ratio) {
RATIO_SHOW_PIN = ratio;
}
public static int getShowPinTextRatio() {
return RATIO_SHOW_PIN_TEXT;
}
public static void setShowPinTextRatio(int ratio) {
RATIO_SHOW_PIN_TEXT = ratio;
}
public Marker getResultMarker() {
return resultMarker;
}
@Deprecated
public Marker getHighlightedMarker() {
return getResultMarker();
}
public void setResultMarker(Marker marker) {
resultMarker = marker;
}
public boolean isResultMarker(Marker marker) {
if (resultMarker == null)
return false;
return resultMarker == marker;
}
public void showResultMarker() {
if (resultMarker != null) {
boolean noDelay = false;
if (isInView(getResultMarker().getPoint()))
noDelay = true;
AnimationBuilder anim = animateScaleAndCenter(getShowTextScale(),
resultMarker.getPoint());
anim.withDuration(750).start();
setMarkerAnimation(noDelay, MapFragment.SOUND_ID_RESULT);
}
}
public void setAndShowResultMarker(Marker marker) {
setResultMarker(marker);
showResultMarker();
}
public void addMarker(Marker m) {
if (!addedMarkerList.contains(m)) {
addedMarkerList.add(m);
}
}
public void addMarker() {
Marker m = getResultMarker();
addMarker(m);
}
public void addMarkers(Collection<? extends Marker> markers) {
for (Marker m : markers) {
addMarker(m);
}
}
public void addMarkers(Marker[] markerArray) {
List<Marker> markerList = Arrays.asList(markerArray);
addMarkers(markerList);
}
public void removeAddedMarker(Marker m) {
if (addedMarkerList.contains(m)) {
addedMarkerList.remove(m);
}
}
public void removeAddedMarkers(Collection<? extends Marker> markers) {
for (Marker m : markers) {
removeAddedMarker(m);
}
}
public void removeAddedMarkers(Marker[] markerArray) {
List<Marker> markerList = Arrays.asList(markerArray);
removeAddedMarkers(markerList);
}
public void removeAddedMarkers() {
addedMarkerList.clear();
}
public boolean isAddedMarker(Marker m) {
return addedMarkerList.contains(m);
}
public boolean isAddedMarker() {
return isAddedMarker(getResultMarker());
}
public void toggleMarker(Marker m) {
if (isAddedMarker(m)) {
removeAddedMarker(m);
mainActivity.playAnimSound(MapFragment.SOUND_ID_REMOVE);
} else {
addMarker(m);
if (!isInView(m.getPoint())) {
AnimationBuilder anim = animateScaleAndCenter(
getShowTextScale(), m.getPoint());
anim.withDuration(750).start();
setMarkerAnimation(false, MapFragment.SOUND_ID_ADD);
} else {
setMarkerAnimation(true, MapFragment.SOUND_ID_ADD);
}
}
invalidate();
}
public void toggleMarker() {
toggleMarker(getResultMarker());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Don't draw pin before image is ready so it doesn't move around during
// setup.
if (!isImageReady()) {
return;
}
for (Marker marker : markerList) {
if (isInView(marker.getPoint())) {
if (isShowPinScale(marker)
&& !(isResultMarker(marker) || addedMarkerList
.contains(marker))) {
if (shouldShowUp(marker))
drawPionterAndText(canvas, marker);
}
}
}
for (Marker marker : addedMarkerList) {
if (isInView(marker.getPoint())) {
if (!isResultMarker(marker)) {
drawMarkerBitmap(canvas, marker);
drawMarkerText(canvas, marker);
}
}
}
Marker marker = getResultMarker();
if (marker != null) {
if (isInView(marker.getPoint())) {
drawMarkerBitmap(canvas, marker);
drawMarkerText(canvas, marker);
}
}
}
private boolean shouldShowUp(Marker marker) {
boolean result = true;
if(marker.getGroupIndex() == Marker.RESIDENCES){
result = settingsManager.showResidences();
}
if (marker instanceof Building) {
String[] childKeys = ((Building) marker).children;
for (String childKey : childKeys) {
Marker child = data.get(childKey);
if (isAddedMarker(child) || isResultMarker(child)) {
result = false;
break;
}
}
}
if (marker instanceof Room)
result = false;
return result;
}
private void drawMarkerBitmap(Canvas canvas, Marker marker) {
Bitmap highlightedPin = getMarkerBitmap(marker);
PointF vPin = sourceToViewCoord(marker.getPoint());
float vX = vPin.x - (highlightedPin.getWidth() / 2);
float vY = vPin.y - highlightedPin.getHeight();
canvas.drawBitmap(highlightedPin, vX, vY, paint);
}
private void drawMarkerText(Canvas canvas, Marker marker) {
String name;
PointF vPin = sourceToViewCoord(marker.getPoint());
if (marker.getShortName().equals("0") || marker.getShortName().isEmpty())
name = marker.getName();
else
name = marker.getShortName();
textPaint.getTextBounds(name, 0, name.length() - 1, bounds);
float tX = vPin.x - bounds.width() / 2;
float tY = vPin.y + bounds.height();
canvas.drawText(name, tX, tY, textPaint);
// canvas.drawText(names[0], tX, tY, strokePaint);
}
private void drawPionterAndText(Canvas canvas, Marker marker) {
Bitmap pin = getPointerBitmap(marker);
PointF vPin = sourceToViewCoord(marker.getPoint());
float vX = vPin.x - (pin.getWidth() / 2);
float vY = vPin.y - (pin.getHeight() / 2);
canvas.drawBitmap(pin, vX, vY, paint);
if (isShowPinTextScale(marker)) {
String name;
if (marker.getShortName().equals("0") || marker.getShortName().isEmpty())
name = marker.getName();
else
name = marker.getShortName();
Paint temp = new Paint(textPaint);
if(marker.getGroupIndex() == Marker.RESIDENCES) temp.setTextSize(12*density);
textPaint.getTextBounds(name, 0, name.length() - 1, bounds);
float tX = vPin.x + pin.getWidth();
float tY = vPin.y + bounds.height() / 2;
canvas.drawText(name, tX, tY, temp);
}
}
private boolean isInView(PointF point) {
int displayWidth = displayMetrics.widthPixels;
int displayHeight = displayMetrics.heightPixels;
int viewX = (int) sourceToViewCoord(point).x;
int viewY = (int) sourceToViewCoord(point).y;
if (viewX > -displayWidth / 3 && viewX < displayWidth && viewY > 0
&& viewY < displayHeight)
return true;
return false;
}
private Marker getNearestMarker(PointF touchPoint) {
Marker resultMarker = null;
float minDist = 100000000f;
for (Marker marker : markerList) {
PointF point = marker.getPoint();
float dist = (float) calculateDistance(point, touchPoint);
if (dist < minDist && isMarkerVisible(marker)) {
minDist = dist;
resultMarker = marker;
}
}
return resultMarker;
}
private double calculateDistance(PointF point1, PointF point2) {
float xDiff = point1.x - point2.x;
float yDiff = point1.y - point2.y;
return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
}
private Bitmap getPointerBitmap(Marker marker) {
int color = marker.getColor();
if (color == Marker.COLOR_BLUE) {
return bluePointer;
} else if (color == Marker.COLOR_YELLOW) {
return yellowPointer;
} else if (color == Marker.COLOR_GREEN) {
return greenPointer;
} else if (color == Marker.COLOR_GRAY) {
return grayPointer;
}
return bluePointer;
}
private Bitmap getMarkerBitmap(Marker marker) {
int color = marker.getColor();
Bitmap markerBitmap = null;
if (color == Marker.COLOR_BLUE) {
markerBitmap = blueMarker;
if (isAddedMarker(marker)){
markerBitmap = blueLockedMarker;
if(convoMarkerList.contains(marker)) markerBitmap = blueConvoMarker;
}
} else if (color == Marker.COLOR_YELLOW) {
markerBitmap = yellowMarker;
if (isAddedMarker(marker))
markerBitmap = yellowLockedMarker;
} else if (color == Marker.COLOR_GREEN) {
markerBitmap = greenMarker;
if (isAddedMarker(marker))
markerBitmap = greenLockedMarker;
} else if (color == Marker.COLOR_GRAY) {
markerBitmap = grayMarker;
if (isAddedMarker(marker))
markerBitmap = grayLockedMarker;
}
if (highlightedMarkerScale != 1.0f && isResultMarker(marker)) {
float w = markerBitmap.getWidth() * highlightedMarkerScale;
float h = markerBitmap.getHeight() * highlightedMarkerScale;
markerBitmap = Bitmap.createScaledBitmap(markerBitmap, (int) w,
(int) h, true);
}
if (isResultMarker(marker)) {
float w = markerBitmap.getWidth() * 1.2f;
float h = markerBitmap.getHeight() * 1.2f;
markerBitmap = Bitmap.createScaledBitmap(markerBitmap, (int) w,
(int) h, true);
}
return markerBitmap;
}
private void setMarkerAnimation(boolean noDelay, int _sound_index) {
final int sound_index = _sound_index;
long delay = 0;
if (!noDelay) {
delay = DELAY_MARKER_ANIMATION;
}
if (android.os.Build.VERSION.SDK_INT >= 11) {
playAnim(delay);
} else {
highlightedMarkerScale = 1.0f;
}
mainActivity.playAnimSoundDelayed(sound_index, delay);
if (isImageReady())
invalidate();
}
@SuppressLint("NewApi")
private void playAnim(long delay) {
highlightedMarkerScale = 0.1f;
ValueAnimator valAnim = new ValueAnimator();
valAnim.setFloatValues(0.1f, 1.0f);
valAnim.setDuration(DURATION_MARKER_ANIMATION);
valAnim.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
highlightedMarkerScale = (Float) animation.getAnimatedValue();
if (isImageReady())
invalidate();
}
});
TimeInterpolator i = new BounceInterpolator();
valAnim.setInterpolator(i);
valAnim.setStartDelay(delay);
valAnim.start();
}
public Runnable getScaleAnim(final float scale){
Runnable anim = new Runnable() {
public void run() {
AnimationBuilder animation = animateScale(scale);
animation
.withDuration(200)
.withEasing(
SubsamplingScaleImageView.EASE_OUT_QUAD)
.start();
}
};
return anim;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if(getTargetMinScale() > getScale()){
setScaleAndCenter(getTargetMinScale(), getCenter());
}
super.onSizeChanged(w, h, oldw, oldh);
}
private void setGestureDetector() {
final GestureDetector gestureDetector = new GestureDetector(
mainActivity.getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (isImageReady()) {
PointF sCoord = viewToSourceCoord(e.getX(),
e.getY());
Marker marker = getNearestMarker(sCoord);
if (isMarkerInTouchRegion(marker, sCoord)) {
// mMainActivity.resultMarker(marker.name);
mainActivity.editText.setText(marker.getName());
mainActivity.displayMap();
}
} else {
}
return true;
}
});
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
final float targetMinScale = getTargetMinScale();
int action = motionEvent.getAction();
if(action== MotionEvent.ACTION_DOWN){
if(motionEvent.getX()<20*density){
getParent().requestDisallowInterceptTouchEvent(false);
return true;
}
else{
// CampusMapView.this.setPanEnabled(true);
}
}
else if(action == MotionEvent.ACTION_UP){
CampusMapView.this.setPanEnabled(true);
}
if (targetMinScale > getScale()) {
callSuperOnTouch(motionEvent);
if (action == MotionEvent.ACTION_UP) {
Runnable anim = getScaleAnim(targetMinScale);
if(isImageReady()) anim.run();
}
return true;
}
return gestureDetector.onTouchEvent(motionEvent);
}
});
}
private void callSuperOnTouch(MotionEvent me) {
super.onTouchEvent(me);
}
private boolean isMarkerInTouchRegion(Marker marker, PointF o) {
if (marker != null) {
PointF point = sourceToViewCoord(marker.getPoint());
PointF origin = sourceToViewCoord(o);
float dist = (float) calculateDistance(point, origin);
if (dist < pointerWidth * density * 2 && isMarkerVisible(marker)) {
return true;
}
}
return false;
}
private boolean isMarkerVisible(Marker marker) {
if (marker == resultMarker)
return true;
if (addedMarkerList.contains(marker))
return true;
if (isShowPinScale(marker) && shouldShowUp(marker))
return true;
return false;
}
private boolean isShowPinScale(Marker m) {
if (specialMarkerList.contains(m))
return true;
PointF left = viewToSourceCoord(0, 0);
PointF right = viewToSourceCoord(getWidth(), 0);
float xDpi = displayMetrics.xdpi;
if ((right.x - left.x) * xDpi / getWidth() < getSWidth()
/ RATIO_SHOW_PIN)
return true;
return false;
}
private boolean isShowPinTextScale(Marker m) {
if (specialMarkerList.contains(m))
return true;
// PointF left = viewToSourceCoord(0, 0);
// PointF right = viewToSourceCoord(getWidth(), 0);
// float xDpi = displayMetrics.xdpi;
// if((right.x-left.x)*xDpi/getWidth() <
// getSWidth()*density/(RATIO_SHOW_PIN_TEXT*2)) return true;
if (getScale() >= (getShowTextScale()))
return true;
return false;
}
private float getShowTextScale() {
float xDpi = displayMetrics.xdpi;
float scale = (RATIO_SHOW_PIN_TEXT * xDpi * 2 / density + 20)
/ getSWidth();
if (scale > getMaxScale()) {
scale = 0.7f * getMaxScale();
}
return scale;
}
// public void setAddedMarkers(String addedMarkerString) {
// addedMarkerList = new ArrayList<Marker>();
// String[] addedMarkerNames = addedMarkerString.split("###");
// for (String s : addedMarkerNames) {
// if (!s.equals("")) {
// Log.d("test123","names = " + s);
// if (data.containsKey(s)) {
// addedMarkerList.add(data.get(s));
// }
// }
// }
// }
//
// public String getAddedMarkerString() {
// String s = "";
// for (Marker m : addedMarkerList) {
// s = s + m.name + "###";
// }
// Log.d("test123","addedMarkerStringGen = " + s);
// return s;
// }
}
/*
Copyright 2014 David Morrissey
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.mrane.zoomview;
import android.graphics.PointF;
import java.io.Serializable;
/**
* Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate.
*/
public class ImageViewState implements Serializable {
/**
*
*/
private static final long serialVersionUID = -4397017033346987595L;
private float scale;
private float centerX;
private float centerY;
private int orientation;
public ImageViewState(float scale, PointF center, int orientation) {
this.scale = scale;
this.centerX = center.x;
this.centerY = center.y;
this.orientation = orientation;
}
public float getScale() {
return scale;
}
public PointF getCenter() {
return new PointF(centerX, centerY);
}
public int getOrientation() {
return orientation;
}
}
/*
Copyright 2014 David Morrissey
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.mrane.zoomview;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.media.ExifInterface;
import android.os.AsyncTask;
import android.os.Build.VERSION;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import app.insti.R.styleable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Displays an image subsampled as necessary to avoid loading too much image data into memory. After a pinch to zoom in,
* a set of image tiles subsampled at higher resolution are loaded and displayed over the base layer. During pinch and
* zoom, tiles off screen or higher/lower resolution than required are discarded from memory.
*
* Tiles over 2048px are not used due to hardware rendering limitations.
*
* This view will not work very well with images that are far larger in one dimension than the other because the tile grid
* for each subsampling level has the same number of rows as columns, so each tile has the same width:height ratio as
* the source image. This could result in image data totalling several times the screen area being loaded.
*
* v prefixes - coordinates, translations and distances measured in screen (view) pixels
* s prefixes - coordinates, translations and distances measured in source image pixels (scaled)
*/
@SuppressWarnings("unused")
public class SubsamplingScaleImageView extends View {
private static final String TAG = SubsamplingScaleImageView.class.getSimpleName();
/** Attempt to use EXIF information on the image to rotate it. Works for external files only. */
public static final int ORIENTATION_USE_EXIF = -1;
/** Display the image file in its native orientation. */
public static final int ORIENTATION_0 = 0;
/** Rotate the image 90 degrees clockwise. */
public static final int ORIENTATION_90 = 90;
/** Rotate the image 180 degrees. */
public static final int ORIENTATION_180 = 180;
/** Rotate the image 270 degrees clockwise. */
public static final int ORIENTATION_270 = 270;
private static final List<Integer> VALID_ORIENTATIONS = Arrays.asList(ORIENTATION_0, ORIENTATION_90, ORIENTATION_180, ORIENTATION_270, ORIENTATION_USE_EXIF);
/** During zoom animation, keep the point of the image that was tapped in the same place, and scale the image around it. */
public static final int ZOOM_FOCUS_FIXED = 1;
/** During zoom animation, move the point of the image that was tapped to the center of the screen. */
public static final int ZOOM_FOCUS_CENTER = 2;
/** Zoom in to and center the tapped point immediately without animating. */
public static final int ZOOM_FOCUS_CENTER_IMMEDIATE = 3;
private static final List<Integer> VALID_ZOOM_STYLES = Arrays.asList(ZOOM_FOCUS_FIXED, ZOOM_FOCUS_CENTER, ZOOM_FOCUS_CENTER_IMMEDIATE);
/** Quadratic ease out. Not recommended for scale animation, but good for panning. */
public static final int EASE_OUT_QUAD = 1;
/** Quadratic ease in and out. */
public static final int EASE_IN_OUT_QUAD = 2;
private static final List<Integer> VALID_EASING_STYLES = Arrays.asList(EASE_IN_OUT_QUAD, EASE_OUT_QUAD);
/** Don't allow the image to be panned off screen. As much of the image as possible is always displayed, centered in the view when it is smaller. This is the best option for galleries. */
public static final int PAN_LIMIT_INSIDE = 1;
/** Allows the image to be panned until it is just off screen, but no further. The edge of the image will stop when it is flush with the screen edge. */
public static final int PAN_LIMIT_OUTSIDE = 2;
/** Allows the image to be panned until a corner reaches the center of the screen but no further. Useful when you want to pan any spot on the image to the exact center of the screen. */
public static final int PAN_LIMIT_CUSTOM = 3;
private static final List<Integer> VALID_PAN_LIMITS = Arrays.asList(PAN_LIMIT_INSIDE, PAN_LIMIT_OUTSIDE, PAN_LIMIT_CUSTOM);
// Overlay tile boundaries and other info
private boolean debug = false;
// Image orientation setting
private int orientation = ORIENTATION_0;
// Max scale allowed (prevent infinite zoom)
private float maxScale = 2F;
// Density to reach before loading higher resolution tiles
private int minimumTileDpi = -1;
// Pan limiting style
private int panLimit = PAN_LIMIT_INSIDE;
// Gesture detection settings
private boolean panEnabled = true;
private boolean zoomEnabled = true;
// Double tap zoom behaviour
private float doubleTapZoomScale = 1F;
private int doubleTapZoomStyle = ZOOM_FOCUS_FIXED;
// Current scale and scale at start of zoom
private float scale;
private float scaleStart;
// Screen coordinate of top-left corner of source image
private PointF vTranslate;
private PointF vTranslateStart;
// Source coordinate to center on, used when new position is set externally before view is ready
private Float pendingScale;
private PointF sPendingCenter;
private PointF sRequestedCenter;
// Source image dimensions and orientation - dimensions relate to the unrotated image
private int sWidth;
private int sHeight;
private int sOrientation;
// Is two-finger zooming in progress
private boolean isZooming;
// Is one-finger panning in progress
private boolean isPanning;
// Max touches used in current gesture
private int maxTouchCount;
// Fling detector
private GestureDetector detector;
// Tile decoder
private BitmapRegionDecoder decoder;
private final Object decoderLock = new Object();
// Sample size used to display the whole image when fully zoomed out
private int fullImageSampleSize;
// Map of zoom level to tile grid
private Map<Integer, List<Tile>> tileMap;
// Debug values
private PointF vCenterStart;
private float vDistStart;
// Scale and center animation tracking
private Anim anim;
// Whether a ready notification has been sent to subclasses
private boolean readySent = false;
// Long click listener
private OnLongClickListener onLongClickListener;
// Long click handler
private Handler handler;
private static final int MESSAGE_LONG_CLICK = 1;
// Paint objects created once and reused for efficiency
private Paint bitmapPaint;
private Paint debugPaint;
public SubsamplingScaleImageView(Context context, AttributeSet attr) {
super(context, attr);
setMinimumDpi(160);
setDoubleTapZoomDpi(160);
this.handler = new Handler(new Handler.Callback() {
public boolean handleMessage(Message message) {
if (message.what == MESSAGE_LONG_CLICK && onLongClickListener != null) {
maxTouchCount = 0;
SubsamplingScaleImageView.super.setOnLongClickListener(onLongClickListener);
performLongClick();
SubsamplingScaleImageView.super.setOnLongClickListener(null);
}
return true;
}
});
this.detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (panEnabled && readySent && vTranslate != null && (Math.abs(e1.getX() - e2.getX()) > 50 || Math.abs(e1.getY() - e2.getY()) > 50) && (Math.abs(velocityX) > 500 || Math.abs(velocityY) > 500) && !isZooming) {
PointF vTranslateEnd = new PointF(vTranslate.x + (velocityX * 0.25f), vTranslate.y + (velocityY * 0.25f));
float sCenterXEnd = ((getWidth()/2) - vTranslateEnd.x)/scale;
float sCenterYEnd = ((getHeight()/2) - vTranslateEnd.y)/scale;
new AnimationBuilder(new PointF(sCenterXEnd, sCenterYEnd)).withEasing(EASE_OUT_QUAD).withPanLimited(false).start();
return true;
}
return super.onFling(e1, e2, velocityX, velocityY);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
performClick();
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
if (zoomEnabled && readySent && vTranslate != null) {
float doubleTapZoomScale = Math.min(maxScale, SubsamplingScaleImageView.this.doubleTapZoomScale);
boolean zoomIn = scale <= doubleTapZoomScale * 0.9;
float targetScale = zoomIn ? doubleTapZoomScale : Math.max(getWidth() / (float) sWidth(), getHeight() / (float) sHeight());
PointF targetSCenter = viewToSourceCoord(new PointF(e.getX(), e.getY()));
if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER_IMMEDIATE) {
setScaleAndCenter(targetScale, targetSCenter);
} else if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER || !zoomIn) {
new AnimationBuilder(targetScale, targetSCenter).withInterruptible(false).start();
} else if (doubleTapZoomStyle == ZOOM_FOCUS_FIXED) {
new AnimationBuilder(targetScale, targetSCenter, new PointF(e.getX(), e.getY())).withInterruptible(false).start();
}
invalidate();
return true;
}
return super.onDoubleTapEvent(e);
}
});
// Handle XML attributes
if (attr != null) {
TypedArray typedAttr = getContext().obtainStyledAttributes(attr, styleable.SubsamplingScaleImageView);
if (typedAttr.hasValue(styleable.SubsamplingScaleImageView_assetName)) {
String assetName = typedAttr.getString(styleable.SubsamplingScaleImageView_assetName);
if (assetName != null && assetName.length() > 0) {
setImageAsset(assetName);
}
}
if (typedAttr.hasValue(styleable.SubsamplingScaleImageView_panEnabled)) {
setPanEnabled(typedAttr.getBoolean(styleable.SubsamplingScaleImageView_panEnabled, true));
}
if (typedAttr.hasValue(styleable.SubsamplingScaleImageView_zoomEnabled)) {
setZoomEnabled(typedAttr.getBoolean(styleable.SubsamplingScaleImageView_zoomEnabled, true));
}
}
}
public SubsamplingScaleImageView(Context context) {
this(context, null);
}
/**
* Sets the image orientation. It's best to call this before setting the image file or asset, because it may waste
* loading of tiles. However, this can be freely called at any time.
*/
public final void setOrientation(int orientation) {
if (!VALID_ORIENTATIONS.contains(orientation)) {
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
this.orientation = orientation;
reset(false);
invalidate();
requestLayout();
}
/**
* Display an image from a file in internal or external storage.
* @param extFile URI of the file to display.
*/
public final void setImageFile(String extFile) {
reset(true);
BitmapInitTask task = new BitmapInitTask(this, getContext(), extFile, false);
task.execute();
invalidate();
}
/**
* Display an image from a file in internal or external storage, starting with a given orientation setting, scale
* and center. This is the best method to use when you want scale and center to be restored after screen orientation
* change; it avoids any redundant loading of tiles in the wrong orientation.
* @param extFile URI of the file to display.
* @param state State to be restored. Nullable.
*/
public final void setImageFile(String extFile, ImageViewState state) {
reset(true);
restoreState(state);
BitmapInitTask task = new BitmapInitTask(this, getContext(), extFile, false);
task.execute();
invalidate();
}
/**
* Display an image from a file in assets.
* @param assetName asset name.
*/
public final void setImageAsset(String assetName) {
setImageAsset(assetName, null);
}
/**
* Display an image from a file in assets, starting with a given orientation setting, scale and center. This is the
* best method to use when you want scale and center to be restored after screen orientation change; it avoids any
* redundant loading of tiles in the wrong orientation.
* @param assetName asset name.
* @param state State to be restored. Nullable.
*/
public final void setImageAsset(String assetName, ImageViewState state) {
reset(true);
restoreState(state);
BitmapInitTask task = new BitmapInitTask(this, getContext(), assetName, true);
task.execute();
invalidate();
}
/**
* Reset all state before setting/changing image or setting new rotation.
*/
private void reset(boolean newImage) {
scale = 0f;
scaleStart = 0f;
vTranslate = null;
vTranslateStart = null;
pendingScale = 0f;
sPendingCenter = null;
sRequestedCenter = null;
isZooming = false;
isPanning = false;
maxTouchCount = 0;
fullImageSampleSize = 0;
vCenterStart = null;
vDistStart = 0;
anim = null;
if (newImage) {
if (decoder != null) {
synchronized (decoderLock) {
decoder.recycle();
decoder = null;
}
}
sWidth = 0;
sHeight = 0;
sOrientation = 0;
readySent = false;
}
if (tileMap != null) {
for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {
for (Tile tile : tileMapEntry.getValue()) {
tile.visible = false;
if (tile.bitmap != null) {
tile.bitmap.recycle();
tile.bitmap = null;
}
}
}
tileMap = null;
}
}
/**
* On resize, preserve center and scale. Various behaviours are possible, override this method to use another.
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (readySent) {
setScaleAndCenter(getScale(), getCenter());
}
}
/**
* Measures the width and height of the view, preserving the aspect ratio of the image displayed if wrap_content is
* used. The image will scale within this box, not resizing the view as it is zoomed.
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
boolean resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
boolean resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
int width = parentWidth;
int height = parentHeight;
if (sWidth > 0 && sHeight > 0) {
if (resizeWidth && resizeHeight) {
width = sWidth();
height = sHeight();
} else if (resizeHeight) {
height = (int)((((double)sHeight()/(double)sWidth()) * width));
} else if (resizeWidth) {
width = (int)((((double)sWidth()/(double)sHeight()) * height));
}
}
width = Math.max(width, getSuggestedMinimumWidth());
height = Math.max(height, getSuggestedMinimumHeight());
setMeasuredDimension(width, height);
}
/**
* Handle touch events. One finger pans, and two finger pinch and zoom plus panning.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
PointF vCenterEnd;
float vDistEnd;
// During non-interruptible anims, ignore all touch events
if (anim != null && !anim.interruptible) {
getParent().requestDisallowInterceptTouchEvent(true);
return true;
} else {
anim = null;
}
// Abort if not ready
if (vTranslate == null) {
return true;
}
// Detect flings, taps and double taps
if (detector == null || detector.onTouchEvent(event)) {
return true;
}
int touchCount = event.getPointerCount();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_1_DOWN:
case MotionEvent.ACTION_POINTER_2_DOWN:
anim = null;
getParent().requestDisallowInterceptTouchEvent(true);
maxTouchCount = Math.max(maxTouchCount, touchCount);
if (touchCount >= 2) {
if (zoomEnabled) {
// Start pinch to zoom. Calculate distance between touch points and center point of the pinch.
float distance = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
scaleStart = scale;
vDistStart = distance;
vTranslateStart = new PointF(vTranslate.x, vTranslate.y);
vCenterStart = new PointF((event.getX(0) + event.getX(1))/2, (event.getY(0) + event.getY(1))/2);
} else {
// Abort all gestures on second touch
maxTouchCount = 0;
}
// Cancel long click timer
handler.removeMessages(MESSAGE_LONG_CLICK);
} else {
// Start one-finger pan
vTranslateStart = new PointF(vTranslate.x, vTranslate.y);
vCenterStart = new PointF(event.getX(), event.getY());
// Start long click timer
handler.sendEmptyMessageDelayed(MESSAGE_LONG_CLICK, 600);
}
return true;
case MotionEvent.ACTION_MOVE:
boolean consumed = false;
if (maxTouchCount > 0) {
if (touchCount >= 2) {
// Calculate new distance between touch points, to scale and pan relative to start values.
vDistEnd = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
vCenterEnd = new PointF((event.getX(0) + event.getX(1))/2, (event.getY(0) + event.getY(1))/2);
if (zoomEnabled && (distance(vCenterStart.x, vCenterEnd.x, vCenterStart.y, vCenterEnd.y) > 5 || Math.abs(vDistEnd - vDistStart) > 5 || isPanning)) {
isZooming = true;
isPanning = true;
consumed = true;
scale = Math.min(maxScale, (vDistEnd / vDistStart) * scaleStart);
if (scale <= minScale()) {
// Minimum scale reached so don't pan. Adjust start settings so any expand will zoom in.
vDistStart = vDistEnd;
scaleStart = minScale();
vCenterStart = vCenterEnd;
vTranslateStart = vTranslate;
} else if (panEnabled) {
// Translate to place the source image coordinate that was at the center of the pinch at the start
// at the center of the pinch now, to give simultaneous pan + zoom.
float vLeftStart = vCenterStart.x - vTranslateStart.x;
float vTopStart = vCenterStart.y - vTranslateStart.y;
float vLeftNow = vLeftStart * (scale/scaleStart);
float vTopNow = vTopStart * (scale/scaleStart);
vTranslate.x = vCenterEnd.x - vLeftNow;
vTranslate.y = vCenterEnd.y - vTopNow;
} else if (sRequestedCenter != null) {
// With a center specified from code, zoom around that point.
vTranslate.x = (getWidth()/2) - (scale * sRequestedCenter.x);
vTranslate.y = (getHeight()/2) - (scale * sRequestedCenter.y);
} else {
// With no requested center, scale around the image center.
vTranslate.x = (getWidth()/2) - (scale * (sWidth()/2));
vTranslate.y = (getHeight()/2) - (scale * (sHeight()/2));
}
fitToBounds(true);
refreshRequiredTiles(false);
}
} else if (!isZooming) {
// One finger pan - translate the image. We do this calculation even with pan disabled so click
// and long click behaviour is preserved.
float dx = Math.abs(event.getX() - vCenterStart.x);
float dy = Math.abs(event.getY() - vCenterStart.y);
if (dx > 5 || dy > 5 || isPanning) {
consumed = true;
vTranslate.x = vTranslateStart.x + (event.getX() - vCenterStart.x);
vTranslate.y = vTranslateStart.y + (event.getY() - vCenterStart.y);
float lastX = vTranslate.x;
float lastY = vTranslate.y;
fitToBounds(true);
if (lastX == vTranslate.x || (lastY == vTranslate.y && dy > 10) || isPanning) {
isPanning = true;
} else if (dx > 5) {
// Haven't panned the image, and we're at the left or right edge. Switch to page swipe.
maxTouchCount = 0;
handler.removeMessages(MESSAGE_LONG_CLICK);
getParent().requestDisallowInterceptTouchEvent(false);
}
if (!panEnabled) {
vTranslate.x = vTranslateStart.x;
vTranslate.y = vTranslateStart.y;
getParent().requestDisallowInterceptTouchEvent(false);
}
refreshRequiredTiles(false);
}
}
}
if (consumed) {
handler.removeMessages(MESSAGE_LONG_CLICK);
invalidate();
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_POINTER_2_UP:
handler.removeMessages(MESSAGE_LONG_CLICK);
if (maxTouchCount > 0 && (isZooming || isPanning)) {
if (isZooming && touchCount == 2) {
// Convert from zoom to pan with remaining touch
isPanning = true;
vTranslateStart = new PointF(vTranslate.x, vTranslate.y);
if (event.getActionIndex() == 1) {
vCenterStart = new PointF(event.getX(0), event.getY(0));
} else {
vCenterStart = new PointF(event.getX(1), event.getY(1));
}
}
if (touchCount < 3) {
// End zooming when only one touch point
isZooming = false;
}
if (touchCount < 2) {
// End panning when no touch points
isPanning = false;
maxTouchCount = 0;
}
// Trigger load of tiles now required
refreshRequiredTiles(true);
return true;
}
if (touchCount == 1) {
isZooming = false;
isPanning = false;
maxTouchCount = 0;
}
return true;
}
return super.onTouchEvent(event);
}
@Override
public void setOnLongClickListener(OnLongClickListener onLongClickListener) {
this.onLongClickListener = onLongClickListener;
}
/**
* Draw method should not be called until the view has dimensions so the first calls are used as triggers to calculate
* the scaling and tiling required. Once the view is setup, tiles are displayed as they are loaded.
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
createPaints();
// If image or view dimensions are not known yet, abort.
if (sWidth == 0 || sHeight == 0 || decoder == null || getWidth() == 0 || getHeight() == 0) {
return;
}
// On first render with no tile map ready, initialise it and kick off async base image loading.
if (tileMap == null) {
initialiseBaseLayer(getMaxBitmapDimensions(canvas));
return;
}
// If waiting to translate to new center position, set translate now
if (sPendingCenter != null && pendingScale != null) {
scale = pendingScale;
vTranslate.x = (getWidth()/2) - (scale * sPendingCenter.x);
vTranslate.y = (getHeight()/2) - (scale * sPendingCenter.y);
sPendingCenter = null;
pendingScale = null;
fitToBounds(true);
refreshRequiredTiles(true);
}
// On first display of base image set up position, and in other cases make sure scale is correct.
fitToBounds(false);
// Everything is set up and coordinates are valid. Inform subclasses.
if (!readySent) {
readySent = true;
new Thread(new Runnable() {
public void run() {
onImageReady();
}
}).start();
}
// If animating scale, calculate current scale and center with easing equations
if (anim != null) {
long scaleElapsed = System.currentTimeMillis() - anim.time;
boolean finished = scaleElapsed > anim.duration;
scaleElapsed = Math.min(scaleElapsed, anim.duration);
scale = ease(anim.easing, scaleElapsed, anim.scaleStart, anim.scaleEnd - anim.scaleStart, anim.duration);
// Apply required animation to the focal point
float vFocusNowX = ease(anim.easing, scaleElapsed, anim.vFocusStart.x, anim.vFocusEnd.x - anim.vFocusStart.x, anim.duration);
float vFocusNowY = ease(anim.easing, scaleElapsed, anim.vFocusStart.y, anim.vFocusEnd.y - anim.vFocusStart.y, anim.duration);
// Find out where the focal point is at this scale and adjust its position to follow the animation path
PointF vFocus = sourceToViewCoord(anim.sCenterEnd);
vTranslate.x -= vFocus.x - vFocusNowX;
vTranslate.y -= vFocus.y - vFocusNowY;
// For translate anims, showing the image non-centered is never allowed, for scaling anims it is during the animation.
fitToBounds(finished || (anim.scaleStart == anim.scaleEnd));
refreshRequiredTiles(finished);
if (finished) {
anim = null;
}
invalidate();
}
// Optimum sample size for current scale
int sampleSize = Math.min(fullImageSampleSize, calculateInSampleSize());
// First check for missing tiles - if there are any we need the base layer underneath to avoid gaps
boolean hasMissingTiles = false;
for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {
if (tileMapEntry.getKey() == sampleSize) {
for (Tile tile : tileMapEntry.getValue()) {
if (tile.visible && (tile.loading || tile.bitmap == null)) {
hasMissingTiles = true;
}
}
}
}
// Render all loaded tiles. LinkedHashMap used for bottom up rendering - lower res tiles underneath.
for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {
if (tileMapEntry.getKey() == sampleSize || hasMissingTiles) {
for (Tile tile : tileMapEntry.getValue()) {
Rect vRect = convertRect(sourceToViewRect(tile.sRect));
if (!tile.loading && tile.bitmap != null) {
canvas.drawBitmap(tile.bitmap, null, vRect, bitmapPaint);
if (debug) {
canvas.drawRect(vRect, debugPaint);
}
} else if (tile.loading && debug) {
canvas.drawText("LOADING", vRect.left + 5, vRect.top + 35, debugPaint);
}
if (tile.visible && debug) {
canvas.drawText("ISS " + tile.sampleSize + " RECT " + tile.sRect.top + "," + tile.sRect.left + "," + tile.sRect.bottom + "," + tile.sRect.right, vRect.left + 5, vRect.top + 15, debugPaint);
}
}
}
}
if (debug) {
canvas.drawText("Scale: " + String.format("%.2f", scale), 5, 15, debugPaint);
canvas.drawText("Translate: " + String.format("%.2f", vTranslate.x) + ":" + String.format("%.2f", vTranslate.y), 5, 35, debugPaint);
PointF center = getCenter();
canvas.drawText("Source center: " + String.format("%.2f", center.x) + ":" + String.format("%.2f", center.y), 5, 55, debugPaint);
if (anim != null) {
PointF vCenterStart = sourceToViewCoord(anim.sCenterStart);
PointF vCenterEndRequested = sourceToViewCoord(anim.sCenterEndRequested);
PointF vCenterEnd = sourceToViewCoord(anim.sCenterEnd);
canvas.drawCircle(vCenterStart.x, vCenterStart.y, 10, debugPaint);
canvas.drawCircle(vCenterEndRequested.x, vCenterEndRequested.y, 20, debugPaint);
canvas.drawCircle(vCenterEnd.x, vCenterEnd.y, 25, debugPaint);
canvas.drawCircle(getWidth()/2, getHeight()/2, 30, debugPaint);
}
}
}
/**
* Creates Paint objects once when first needed.
*/
private void createPaints() {
if (bitmapPaint == null) {
bitmapPaint = new Paint();
bitmapPaint.setAntiAlias(true);
bitmapPaint.setFilterBitmap(true);
bitmapPaint.setDither(true);
}
if (debugPaint == null && debug) {
debugPaint = new Paint();
debugPaint.setTextSize(18);
debugPaint.setColor(Color.MAGENTA);
debugPaint.setStyle(Style.STROKE);
}
}
/**
* Called on first draw when the view has dimensions. Calculates the initial sample size and starts async loading of
* the base layer image - the whole source subsampled as necessary.
*/
private synchronized void initialiseBaseLayer(Point maxTileDimensions) {
fitToBounds(true);
fullImageSampleSize = calculateInSampleSize();
// Load double resolution - next level will be split into four tiles and at the center all four are required,
// so don't bother with tiling until the next level 16 tiles are needed.
if (fullImageSampleSize > 1) {
fullImageSampleSize /= 2;
}
initialiseTileMap(maxTileDimensions);
List<Tile> baseGrid = tileMap.get(fullImageSampleSize);
for (Tile baseTile : baseGrid) {
BitmapTileTask task = new BitmapTileTask(this, decoder, decoderLock, baseTile);
task.execute();
}
}
/**
* Loads the optimum tiles for display at the current scale and translate, so the screen can be filled with tiles
* that are at least as high resolution as the screen. Frees up bitmaps that are now off the screen.
* @param load Whether to load the new tiles needed. Use false while scrolling/panning for performance.
*/
private void refreshRequiredTiles(boolean load) {
int sampleSize = Math.min(fullImageSampleSize, calculateInSampleSize());
RectF vVisRect = new RectF(0, 0, getWidth(), getHeight());
RectF sVisRect = viewToSourceRect(vVisRect);
// Load tiles of the correct sample size that are on screen. Discard tiles off screen, and those that are higher
// resolution than required, or lower res than required but not the base layer, so the base layer is always present.
for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {
for (Tile tile : tileMapEntry.getValue()) {
if (tile.sampleSize < sampleSize || (tile.sampleSize > sampleSize && tile.sampleSize != fullImageSampleSize)) {
tile.visible = false;
if (tile.bitmap != null) {
tile.bitmap.recycle();
tile.bitmap = null;
}
}
if (tile.sampleSize == sampleSize) {
if (RectF.intersects(sVisRect, convertRect(tile.sRect))) {
tile.visible = true;
if (!tile.loading && tile.bitmap == null && load) {
BitmapTileTask task = new BitmapTileTask(this, decoder, decoderLock, tile);
task.execute();
}
} else if (tile.sampleSize != fullImageSampleSize) {
tile.visible = false;
if (tile.bitmap != null) {
tile.bitmap.recycle();
tile.bitmap = null;
}
}
} else if (tile.sampleSize == fullImageSampleSize) {
tile.visible = true;
}
}
}
}
/**
* Calculates sample size to fit the source image in given bounds.
*/
private int calculateInSampleSize() {
float adjustedScale = scale;
if (minimumTileDpi > 0) {
DisplayMetrics metrics = getResources().getDisplayMetrics();
float averageDpi = (metrics.xdpi + metrics.ydpi)/2;
adjustedScale = (minimumTileDpi/averageDpi) * scale;
}
int reqWidth = (int)(sWidth() * adjustedScale);
int reqHeight = (int)(sHeight() * adjustedScale);
// Raw height and width of image
int inSampleSize = 1;
if (reqWidth == 0 || reqHeight == 0) {
return 32;
}
if (sHeight() > reqHeight || sWidth() > reqWidth) {
// Calculate ratios of height and width to requested height and width
final int heightRatio = Math.round((float) sHeight() / (float) reqHeight);
final int widthRatio = Math.round((float) sWidth() / (float) reqWidth);
// Choose the smallest ratio as inSampleSize value, this will guarantee
// a final image with both dimensions larger than or equal to the
// requested height and width.
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
// We want the actual sample size that will be used, so round down to nearest power of 2.
int power = 1;
while (power * 2 < inSampleSize) {
power = power * 2;
}
return power;
}
/**
* Adjusts hypothetical future scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale
* is set so one dimension fills the view and the image is centered on the other dimension. Used to calculate what the target of an
* animation should be.
* @param center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached.
* @param scaleAndTranslate The scale we want and the translation we're aiming for. The values are adjusted to be valid.
*/
private void fitToBounds(boolean center, ScaleAndTranslate scaleAndTranslate) {
if (panLimit == PAN_LIMIT_OUTSIDE && isImageReady()) {
center = false;
}
PointF vTranslate = scaleAndTranslate.translate;
float scale = limitedScale(scaleAndTranslate.scale);
float scaleWidth = scale * sWidth();
float scaleHeight = scale * sHeight();
if (panLimit == PAN_LIMIT_CUSTOM && isImageReady()) {
vTranslate.x = Math.max(vTranslate.x, getWidth() - scaleWidth);
vTranslate.y = Math.max(vTranslate.y, 9*getHeight()/10 - scaleHeight);
} else if (center) {
vTranslate.x = Math.max(vTranslate.x, getWidth() - scaleWidth);
vTranslate.y = Math.max(vTranslate.y, getHeight() - scaleHeight);
} else {
vTranslate.x = Math.max(vTranslate.x, -scaleWidth);
vTranslate.y = Math.max(vTranslate.y, -scaleHeight);
}
float maxTx;
float maxTy;
if (panLimit == PAN_LIMIT_CUSTOM && isImageReady()) {
maxTx = Math.max(0, 0);
maxTy = Math.max(0, getHeight()/10);
} else if (center) {
maxTx = Math.max(0, (getWidth() - scaleWidth) / 2);
maxTy = Math.max(0, (getHeight() - scaleHeight) / 2);
} else {
maxTx = Math.max(0, getWidth());
maxTy = Math.max(0, getHeight());
}
vTranslate.x = Math.min(vTranslate.x, maxTx);
vTranslate.y = Math.min(vTranslate.y, maxTy);
scaleAndTranslate.scale = scale;
}
/**
* Adjusts current scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale
* is set so one dimension fills the view and the image is centered on the other dimension.
* @param center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached.
*/
private void fitToBounds(boolean center) {
if (vTranslate == null) {
vTranslate = new PointF(0, 0);
}
ScaleAndTranslate input = new ScaleAndTranslate(scale, vTranslate);
fitToBounds(center, input);
scale = input.scale;
}
/**
* Once source image and view dimensions are known, creates a map of sample size to tile grid.
*/
private void initialiseTileMap(Point maxTileDimensions) {
this.tileMap = new LinkedHashMap<Integer, List<Tile>>();
int sampleSize = fullImageSampleSize;
int xTiles = 1;
int yTiles = 1;
while (true) {
int sTileWidth = sWidth()/xTiles;
int sTileHeight = sHeight()/yTiles;
int subTileWidth = sTileWidth/sampleSize;
int subTileHeight = sTileHeight/sampleSize;
while (subTileWidth > maxTileDimensions.x || (subTileWidth > getWidth() * 1.25 && sampleSize < fullImageSampleSize)) {
xTiles += 1;
sTileWidth = sWidth()/xTiles;
subTileWidth = sTileWidth/sampleSize;
}
while (subTileHeight > maxTileDimensions.y || (subTileHeight > getHeight() * 1.25 && sampleSize < fullImageSampleSize)) {
yTiles += 1;
sTileHeight = sHeight()/yTiles;
subTileHeight = sTileHeight/sampleSize;
}
List<Tile> tileGrid = new ArrayList<Tile>(xTiles * yTiles);
for (int x = 0; x < xTiles; x++) {
for (int y = 0; y < yTiles; y++) {
Tile tile = new Tile();
tile.sampleSize = sampleSize;
tile.visible = sampleSize == fullImageSampleSize;
tile.sRect = new Rect(
x * sTileWidth,
y * sTileHeight,
(x + 1) * sTileWidth,
(y + 1) * sTileHeight
);
tileGrid.add(tile);
}
}
tileMap.put(sampleSize, tileGrid);
if (sampleSize == 1) {
break;
} else {
sampleSize /= 2;
}
}
}
/**
* Called by worker task when decoder is ready and image size and EXIF orientation is known.
*/
private void onImageInited(BitmapRegionDecoder decoder, int sWidth, int sHeight, int sOrientation) {
this.decoder = decoder;
this.sWidth = sWidth;
this.sHeight = sHeight;
this.sOrientation = sOrientation;
requestLayout();
invalidate();
}
/**
* Called by worker task when a tile has loaded. Redraws the view.
*/
private void onTileLoaded() {
invalidate();
}
/**
* Async task used to get image details without blocking the UI thread.
*/
private static class BitmapInitTask extends AsyncTask<Void, Void, int[]> {
private final WeakReference<SubsamplingScaleImageView> viewRef;
private final WeakReference<Context> contextRef;
private final String source;
private final boolean sourceIsAsset;
private BitmapRegionDecoder decoder;
public BitmapInitTask(SubsamplingScaleImageView view, Context context, String source, boolean sourceIsAsset) {
this.viewRef = new WeakReference<SubsamplingScaleImageView>(view);
this.contextRef = new WeakReference<Context>(context);
this.source = source;
this.sourceIsAsset = sourceIsAsset;
}
@Override
protected int[] doInBackground(Void... params) {
try {
if (viewRef != null && contextRef != null) {
Context context = contextRef.get();
if (context != null) {
int exifOrientation = ORIENTATION_0;
if (sourceIsAsset) {
decoder = BitmapRegionDecoder.newInstance(context.getAssets().open(source, AssetManager.ACCESS_RANDOM), true);
} else {
decoder = BitmapRegionDecoder.newInstance(source, true);
try {
ExifInterface exifInterface = new ExifInterface(source);
int orientationAttr = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
if (orientationAttr == ExifInterface.ORIENTATION_NORMAL) {
exifOrientation = ORIENTATION_0;
} else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_90) {
exifOrientation = ORIENTATION_90;
} else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_180) {
exifOrientation = ORIENTATION_180;
} else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_270) {
exifOrientation = ORIENTATION_270;
} else {
Log.w(TAG, "Unsupported EXIF orientation: " + orientationAttr);
}
} catch (Exception e) {
Log.w(TAG, "Could not get EXIF orientation of image");
}
}
return new int[] { decoder.getWidth(), decoder.getHeight(), exifOrientation };
}
}
} catch (Exception e) {
Log.e(TAG, "Failed to initialise bitmap decoder", e);
}
return null;
}
@Override
protected void onPostExecute(int[] xyo) {
if (viewRef != null && decoder != null) {
final SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get();
if (subsamplingScaleImageView != null && decoder != null && xyo != null && xyo.length == 3) {
subsamplingScaleImageView.onImageInited(decoder, xyo[0], xyo[1], xyo[2]);
}
}
}
}
/**
* Async task used to load images without blocking the UI thread.
*/
private static class BitmapTileTask extends AsyncTask<Void, Void, Bitmap> {
private final WeakReference<SubsamplingScaleImageView> viewRef;
private final WeakReference<BitmapRegionDecoder> decoderRef;
private final WeakReference<Object> decoderLockRef;
private final WeakReference<Tile> tileRef;
public BitmapTileTask(SubsamplingScaleImageView view, BitmapRegionDecoder decoder, Object decoderLock, Tile tile) {
this.viewRef = new WeakReference<SubsamplingScaleImageView>(view);
this.decoderRef = new WeakReference<BitmapRegionDecoder>(decoder);
this.decoderLockRef = new WeakReference<Object>(decoderLock);
this.tileRef = new WeakReference<Tile>(tile);
tile.loading = true;
}
@Override
protected Bitmap doInBackground(Void... params) {
try {
if (decoderRef != null && tileRef != null && viewRef != null) {
final BitmapRegionDecoder decoder = decoderRef.get();
final Object decoderLock = decoderLockRef.get();
final Tile tile = tileRef.get();
final SubsamplingScaleImageView view = viewRef.get();
if (decoder != null && decoderLock != null && tile != null && view != null && !decoder.isRecycled()) {
synchronized (decoderLock) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = tile.sampleSize;
options.inPreferredConfig = Config.RGB_565;
options.inDither = true;
Bitmap bitmap = decoder.decodeRegion(view.fileSRect(tile.sRect), options);
int rotation = view.getRequiredRotation();
if (rotation != 0) {
Matrix matrix = new Matrix();
matrix.postRotate(rotation);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
return bitmap;
}
} else if (tile != null) {
tile.loading = false;
}
}
} catch (Exception e) {
Log.e(TAG, "Failed to decode tile", e);
}
return null;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (viewRef != null && tileRef != null && bitmap != null) {
final SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get();
final Tile tile = tileRef.get();
if (subsamplingScaleImageView != null && tile != null) {
tile.bitmap = bitmap;
tile.loading = false;
subsamplingScaleImageView.onTileLoaded();
}
}
}
}
private static class Tile {
private Rect sRect;
private int sampleSize;
private Bitmap bitmap;
private boolean loading;
private boolean visible;
}
private static class Anim {
private float scaleStart; // Scale at start of anim
private float scaleEnd; // Scale at end of anim (target)
private PointF sCenterStart; // Source center point at start
private PointF sCenterEnd; // Source center point at end, adjusted for pan limits
private PointF sCenterEndRequested; // Source center point that was requested, without adjustment
private PointF vFocusStart; // View point that was double tapped
private PointF vFocusEnd; // Where the view focal point should be moved to during the anim
private long duration = 500; // How long the anim takes
private boolean interruptible = true; // Whether the anim can be interrupted by a touch
private int easing = EASE_IN_OUT_QUAD; // Easing style
private long time = System.currentTimeMillis(); // Start time
}
private static class ScaleAndTranslate {
private ScaleAndTranslate(float scale, PointF translate) {
this.scale = scale;
this.translate = translate;
}
private float scale;
private PointF translate;
}
/**
* Set scale, center and orientation from saved state.
*/
private void restoreState(ImageViewState state) {
if (state != null && state.getCenter() != null && VALID_ORIENTATIONS.contains(state.getOrientation())) {
this.orientation = state.getOrientation();
this.pendingScale = state.getScale();
this.sPendingCenter = state.getCenter();
invalidate();
}
}
/**
* In SDK 14 and above, use canvas max bitmap width and height instead of the default 2048, to avoid redundant tiling.
*/
private Point getMaxBitmapDimensions(Canvas canvas) {
if (VERSION.SDK_INT >= 14) {
try {
int maxWidth = (Integer)Canvas.class.getMethod("getMaximumBitmapWidth").invoke(canvas);
int maxHeight = (Integer)Canvas.class.getMethod("getMaximumBitmapHeight").invoke(canvas);
return new Point(maxWidth, maxHeight);
} catch (Exception e) {
// Return default
}
}
return new Point(2048, 2048);
}
/**
* Get source width taking rotation into account.
*/
private int sWidth() {
int rotation = getRequiredRotation();
if (rotation == 90 || rotation == 270) {
return sHeight;
} else {
return sWidth;
}
}
/**
* Get source height taking rotation into account.
*/
private int sHeight() {
int rotation = getRequiredRotation();
if (rotation == 90 || rotation == 270) {
return sWidth;
} else {
return sHeight;
}
}
/**
* Converts source rectangle from tile, which treats the image file as if it were in the correct orientation already,
* to the rectangle of the image that needs to be loaded.
*/
private Rect fileSRect(Rect sRect) {
if (getRequiredRotation() == 0) {
return sRect;
} else if (getRequiredRotation() == 90) {
return new Rect(sRect.top, sHeight - sRect.right, sRect.bottom, sHeight - sRect.left);
} else if (getRequiredRotation() == 180) {
return new Rect(sWidth - sRect.right, sHeight - sRect.bottom, sWidth - sRect.left, sHeight - sRect.top);
} else {
return new Rect(sWidth - sRect.bottom, sRect.left, sWidth - sRect.top, sRect.right);
}
}
/**
* Determines the rotation to be applied to tiles, based on EXIF orientation or chosen setting.
*/
private int getRequiredRotation() {
if (orientation == ORIENTATION_USE_EXIF) {
return sOrientation;
} else {
return orientation;
}
}
/**
* Pythagoras distance between two points.
*/
private float distance(float x0, float x1, float y0, float y1) {
float x = x0 - x1;
float y = y0 - y1;
return (float)Math.sqrt(x * x + y * y);
}
/**
* Convert screen coordinate to source coordinate.
*/
public final PointF viewToSourceCoord(PointF vxy) {
return viewToSourceCoord(vxy.x, vxy.y);
}
/**
* Convert screen coordinate to source coordinate.
*/
public final PointF viewToSourceCoord(float vx, float vy) {
if (vTranslate == null) {
return null;
}
float sx = (vx - vTranslate.x)/scale;
float sy = (vy - vTranslate.y)/scale;
return new PointF(sx, sy);
}
/**
* Convert source coordinate to screen coordinate.
*/
public final PointF sourceToViewCoord(PointF sxy) {
return sourceToViewCoord(sxy.x, sxy.y);
}
/**
* Convert source coordinate to screen coordinate.
*/
public final PointF sourceToViewCoord(float sx, float sy) {
if (vTranslate == null) {
return null;
}
float vx = (sx * scale) + vTranslate.x;
float vy = (sy * scale) + vTranslate.y;
return new PointF(vx, vy);
}
/**
* Convert source rect to screen rect.
*/
private RectF sourceToViewRect(Rect sRect) {
return sourceToViewRect(convertRect(sRect));
}
/**
* Convert source rect to screen rect.
*/
private RectF sourceToViewRect(RectF sRect) {
PointF vLT = sourceToViewCoord(new PointF(sRect.left, sRect.top));
PointF vRB = sourceToViewCoord(new PointF(sRect.right, sRect.bottom));
return new RectF(vLT.x, vLT.y, vRB.x, vRB.y);
}
/**
* Convert screen rect to source rect.
*/
private RectF viewToSourceRect(RectF vRect) {
PointF sLT = viewToSourceCoord(new PointF(vRect.left, vRect.top));
PointF sRB = viewToSourceCoord(new PointF(vRect.right, vRect.bottom));
return new RectF(sLT.x, sLT.y, sRB.x, sRB.y);
}
/**
* Int to float rect conversion.
*/
private RectF convertRect(Rect rect) {
return new RectF(rect.left, rect.top, rect.right, rect.bottom);
}
/**
* Float to int rect conversion.
*/
private Rect convertRect(RectF rect) {
return new Rect((int)rect.left, (int)rect.top, (int)rect.right, (int)rect.bottom);
}
/**
* Get the translation required to place a given source coordinate at the center of the screen. Accepts the desired
* scale as an argument, so this is independent of current translate and scale. The result is fitted to bounds, putting
* the image point as near to the screen center as permitted.
*/
private PointF vTranslateForSCenter(PointF sCenter, float scale) {
PointF vTranslate = new PointF((getWidth()/2) - (sCenter.x * scale), (getHeight()/2) - (sCenter.y * scale));
ScaleAndTranslate sat = new ScaleAndTranslate(scale, vTranslate);
fitToBounds(true, sat);
return vTranslate;
}
/**
* Given a requested source center and scale, calculate what the actual center will have to be to keep the image in
* pan limits, keeping the requested center as near to the middle of the screen as allowed.
*/
private PointF limitedSCenter(PointF sCenter, float scale) {
PointF vTranslate = vTranslateForSCenter(sCenter, scale);
int mY = getHeight()/2;
float sx = ((getWidth()/2) - vTranslate.x)/scale;
float sy = ((getHeight()/2) - vTranslate.y)/scale;
return new PointF(sx, sy);
}
/**
* Returns the minimum allowed scale.
*/
private float minScale() {
return Math.min(getWidth() / (float) sWidth(), (getHeight())/ (float) sHeight());
}
/**
* Adjust a requested scale to be within the allowed limits.
*/
private float limitedScale(float targetScale) {
targetScale = Math.max(minScale(), targetScale);
targetScale = Math.min(maxScale, targetScale);
return targetScale;
}
/**
* Apply a selected type of easing.
* @param type Easing type, from static fields
* @param time Elapsed time
* @param from Start value
* @param change Target value
* @param duration Anm duration
* @return Current value
*/
private float ease(int type, long time, float from, float change, long duration) {
switch (type) {
case EASE_IN_OUT_QUAD:
return easeInOutQuad(time, from, change, duration);
case EASE_OUT_QUAD:
return easeOutQuad(time, from, change, duration);
default:
throw new IllegalStateException("Unexpected easing type: " + type);
}
}
/**
* Quadratic easing for fling. With thanks to Robert Penner - http://gizma.com/easing/
* @param time Elapsed time
* @param from Start value
* @param change Target value
* @param duration Anm duration
* @return Current value
*/
private float easeOutQuad(long time, float from, float change, long duration) {
float progress = (float)time/(float)duration;
return -change * progress*(progress-2) + from;
}
/**
* Quadratic easing for scale and center animations. With thanks to Robert Penner - http://gizma.com/easing/
* @param time Elapsed time
* @param from Start value
* @param change Target value
* @param duration Anm duration
* @return Current value
*/
private float easeInOutQuad(long time, float from, float change, long duration) {
float timeF = time/(duration/2f);
if (timeF < 1) {
return (change/2f * timeF * timeF) + from;
} else {
timeF--;
return (-change/2f) * (timeF * (timeF - 2) - 1) + from;
}
}
/**
* Set the pan limiting style. See static fields. Normally {@link #PAN_LIMIT_INSIDE} is best, for image galleries.
*/
public final void setPanLimit(int panLimit) {
if (!VALID_PAN_LIMITS.contains(panLimit)) {
throw new IllegalArgumentException("Invalid pan limit: " + panLimit);
}
this.panLimit = panLimit;
if (isImageReady()) {
fitToBounds(true);
invalidate();
}
}
/**
* Set the maximum scale allowed. A value of 1 means 1:1 pixels at maximum scale. You may wish to set this according
* to screen density - on a retina screen, 1:1 may still be too small. Consider using {@link #setMinimumDpi(int)},
* which is density aware.
*/
public final void setMaxScale(float maxScale) {
this.maxScale = maxScale;
}
/**
* Returns the maximum allowed scale.
*/
public float getMaxScale() {
return maxScale;
}
/**
* This is a screen density aware alternative to {@link #setMaxScale(float)}; it allows you to express the maximum
* allowed scale in terms of the minimum pixel density. This avoids the problem of 1:1 scale still being
* too small on a high density screen. A sensible starting point is 160 - the default used by this view.
* @param dpi Source image pixel density at maximum zoom.
*/
public final void setMinimumDpi(int dpi) {
DisplayMetrics metrics = getResources().getDisplayMetrics();
float averageDpi = (metrics.xdpi + metrics.ydpi)/2;
setMaxScale(averageDpi/dpi);
}
/**
* Returns the minimum allowed scale.
*/
public final float getMinScale() {
return minScale();
}
/**
* By default, image tiles are at least as high resolution as the screen. For a retina screen this may not be
* necessary, and may increase the likelihood of an OutOfMemoryError. This method sets a DPI at which higher
* resolution tiles should be loaded. Using a lower number will on average use less memory but result in a lower
* quality image. 160-240dpi will usually be enough.
* @param minimumTileDpi Tile loading threshold.
*/
public void setMinimumTileDpi(int minimumTileDpi) {
DisplayMetrics metrics = getResources().getDisplayMetrics();
float averageDpi = (metrics.xdpi + metrics.ydpi)/2;
this.minimumTileDpi = (int)Math.min(averageDpi, minimumTileDpi);
if (isImageReady()) {
reset(false);
invalidate();
}
}
/**
* Returns the source point at the center of the view.
*/
public final PointF getCenter() {
int mX = getWidth()/2;
int mY = getHeight()/2;
return viewToSourceCoord(mX, mY);
}
/**
* Returns the current scale value.
*/
public final float getScale() {
return scale;
}
/**
* Externally change the scale and translation of the source image. This may be used with getCenter() and getScale()
* to restore the scale and zoom after a screen rotate.
* @param scale New scale to set.
* @param sCenter New source image coordinate to center on the screen, subject to boundaries.
*/
public final void setScaleAndCenter(float scale, PointF sCenter) {
this.anim = null;
this.pendingScale = scale;
this.sPendingCenter = sCenter;
this.sRequestedCenter = sCenter;
invalidate();
}
/**
* Fully zoom out and return the image to the middle of the screen. This might be useful if you have a view pager
* and want images to be reset when the user has moved to another page.
*/
public final void resetScaleAndCenter() {
this.anim = null;
this.pendingScale = limitedScale(0);
if (isImageReady()) {
this.sPendingCenter = new PointF(sWidth()/2, sHeight()/2);
} else {
this.sPendingCenter = new PointF(0, 0);
}
invalidate();
}
/**
* Subclasses can override this method to be informed when the view is set up and ready for rendering, so they can
* skip their own rendering until the base layer (and its scale and translate) are known.
*/
protected void onImageReady() {
}
/**
* Call to find whether the view is initialised and ready for rendering tiles.
*/
public final boolean isImageReady() {
return readySent && vTranslate != null && tileMap != null && sWidth > 0 && sHeight > 0;
}
/**
* Get source width, ignoring orientation. If {@link #getOrientation()} returns 90 or 270, you can use {@link #getSHeight()}
* for the apparent width.
*/
public final int getSWidth() {
return sWidth;
}
/**
* Get source height, ignoring orientation. If {@link #getOrientation()} returns 90 or 270, you can use {@link #getSWidth()}
* for the apparent height.
*/
public final int getSHeight() {
return sHeight;
}
/**
* Returns the orientation setting. This can return {@link #ORIENTATION_USE_EXIF}, in which case it doesn't tell you
* the applied orientation of the image. For that, use {@link #getAppliedOrientation()}.
*/
public final int getOrientation() {
return orientation;
}
/**
* Returns the actual orientation of the image relative to the source file. This will be based on the source file's
* EXIF orientation if you're using ORIENTATION_USE_EXIF. Values are 0, 90, 180, 270.
*/
public final int getAppliedOrientation() {
return getRequiredRotation();
}
/**
* Get the current state of the view (scale, center, orientation) for restoration after rotate. Will return null if
* the view is not ready.
*/
public final ImageViewState getState() {
if (vTranslate != null && sWidth > 0 && sHeight > 0) {
return new ImageViewState(getScale(), getCenter(), getOrientation());
}
return null;
}
/**
* Returns true if zoom gesture detection is enabled.
*/
public final boolean isZoomEnabled() {
return zoomEnabled;
}
/**
* Enable or disable zoom gesture detection. Disabling zoom locks the the current scale.
*/
public final void setZoomEnabled(boolean zoomEnabled) {
this.zoomEnabled = zoomEnabled;
}
/**
* Returns true if pan gesture detection is enabled.
*/
public final boolean isPanEnabled() {
return panEnabled;
}
/**
* Enable or disable pan gesture detection. Disabling pan causes the image to be centered.
*/
public final void setPanEnabled(boolean panEnabled) {
this.panEnabled = panEnabled;
if (!panEnabled && vTranslate != null) {
vTranslate.x = (getWidth()/2) - (scale * (sWidth()/2));
vTranslate.y = (getHeight()/2) - (scale * (sHeight()/2));
if (isImageReady()) {
refreshRequiredTiles(true);
invalidate();
}
}
}
/**
* Set the scale the image will zoom in to when double tapped. This also the scale point where a double tap is interpreted
* as a zoom out gesture - if the scale is greater than 90% of this value, a double tap zooms out. Avoid using values
* greater than the max zoom.
* @param doubleTapZoomScale New value for double tap gesture zoom scale.
*/
public final void setDoubleTapZoomScale(float doubleTapZoomScale) {
this.doubleTapZoomScale = doubleTapZoomScale;
}
/**
* A density aware alternative to {@link #setDoubleTapZoomScale(float)}; this allows you to express the scale the
* image will zoom in to when double tapped in terms of the image pixel density. Values lower than the max scale will
* be ignored. A sensible starting point is 160 - the default used by this view.
* @param dpi New value for double tap gesture zoom scale.
*/
public final void setDoubleTapZoomDpi(int dpi) {
DisplayMetrics metrics = getResources().getDisplayMetrics();
float averageDpi = (metrics.xdpi + metrics.ydpi)/2;
setDoubleTapZoomScale(averageDpi/dpi);
}
/**
* Set the type of zoom animation to be used for double taps. See static fields.
* @param doubleTapZoomStyle New value for zoom style.
*/
public final void setDoubleTapZoomStyle(int doubleTapZoomStyle) {
if (!VALID_ZOOM_STYLES.contains(doubleTapZoomStyle)) {
throw new IllegalArgumentException("Invalid zoom style: " + doubleTapZoomStyle);
}
this.doubleTapZoomStyle = doubleTapZoomStyle;
}
/**
* Enables visual debugging, showing tile boundaries and sizes.
*/
public final void setDebug(boolean debug) {
this.debug = debug;
}
/**
* Creates a panning animation builder, that when started will animate the image to place the given coordinates of
* the image in the center of the screen. If doing this would move the image beyond the edges of the screen, the
* image is instead animated to move the center point as near to the center of the screen as is allowed - it's
* guaranteed to be on screen.
* @param sCenter Target center point
* @return {@link AnimationBuilder} instance. Call {@link com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.AnimationBuilder#start()} to start the anim.
*/
public AnimationBuilder animateCenter(PointF sCenter) {
if (!isImageReady()) {
return null;
}
return new AnimationBuilder(sCenter);
}
/**
* Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image
* beyond the panning limits, the image is automatically panned during the animation.
* @param scale Target scale.
* @return {@link AnimationBuilder} instance. Call {@link com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.AnimationBuilder#start()} to start the anim.
*/
public AnimationBuilder animateScale(float scale) {
if (!isImageReady()) {
return null;
}
return new AnimationBuilder(scale);
}
/**
* Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image
* beyond the panning limits, the image is automatically panned during the animation.
* @param scale Target scale.
* @return {@link AnimationBuilder} instance. Call {@link com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.AnimationBuilder#start()} to start the anim.
*/
public AnimationBuilder animateScaleAndCenter(float scale, PointF sCenter) {
if (!isImageReady()) {
return null;
}
return new AnimationBuilder(scale, sCenter);
}
/**
* Builder class used to set additional options for a scale animation. Create an instance using {@link #animateScale(float)},
* then set your options and call {@link #start()}.
*/
public final class AnimationBuilder {
private final float targetScale;
private final PointF targetSCenter;
private final PointF vFocus;
private long duration = 500;
private int easing = EASE_IN_OUT_QUAD;
private boolean interruptible = true;
private boolean panLimited = true;
private AnimationBuilder(PointF sCenter) {
this.targetScale = scale;
this.targetSCenter = sCenter;
this.vFocus = null;
}
private AnimationBuilder(float scale) {
this.targetScale = scale;
this.targetSCenter = getCenter();
this.vFocus = null;
}
private AnimationBuilder(float scale, PointF sCenter) {
this.targetScale = scale;
this.targetSCenter = sCenter;
this.vFocus = null;
}
private AnimationBuilder(float scale, PointF sCenter, PointF vFocus) {
this.targetScale = scale;
this.targetSCenter = sCenter;
this.vFocus = vFocus;
}
/**
* Desired duration of the anim in milliseconds. Default is 500.
* @param duration duration in milliseconds.
* @return this builder for method chaining.
*/
public AnimationBuilder withDuration(long duration) {
this.duration = duration;
return this;
}
/**
* Whether the animation can be interrupted with a touch. Default is true.
* @param interruptible interruptible flag.
* @return this builder for method chaining.
*/
public AnimationBuilder withInterruptible(boolean interruptible) {
this.interruptible = interruptible;
return this;
}
/**
* Set the easing style. See static fields. {@link #EASE_IN_OUT_QUAD} is recommended, and the default.
* @param easing easing style.
* @return this builder for method chaining.
*/
public AnimationBuilder withEasing(int easing) {
if (!VALID_EASING_STYLES.contains(easing)) {
throw new IllegalArgumentException("Unknown easing type: " + easing);
}
this.easing = easing;
return this;
}
/**
* Only for internal use. When set to true, the animation proceeds towards the actual end point - the nearest
* point to the center allowed by pan limits. When false, animation is in the direction of the requested end
* point and is stopped when the limit for each axis is reached. The latter behaviour is used for flings but
* nothing else.
*/
private AnimationBuilder withPanLimited(boolean panLimited) {
this.panLimited = panLimited;
return this;
}
/**
* Starts the animation.
*/
public void start() {
float targetScale = limitedScale(this.targetScale);
PointF targetSCenter = panLimited ? limitedSCenter(this.targetSCenter, targetScale) : this.targetSCenter;
anim = new Anim();
anim.scaleStart = scale;
anim.scaleEnd = targetScale;
anim.time = System.currentTimeMillis();
anim.sCenterEndRequested = targetSCenter;
anim.sCenterStart = getCenter();
anim.sCenterEnd = targetSCenter;
anim.vFocusStart = sourceToViewCoord(targetSCenter);
anim.vFocusEnd = new PointF(
getWidth()/2,
getHeight()/2
);
anim.duration = duration;
anim.interruptible = interruptible;
anim.easing = easing;
anim.time = System.currentTimeMillis();
if (vFocus != null) {
// Calculate where translation will be at the end of the anim
float vTranslateXEnd = vFocus.x - (targetScale * anim.sCenterStart.x);
float vTranslateYEnd = vFocus.y - (targetScale * anim.sCenterStart.y);
ScaleAndTranslate satEnd = new ScaleAndTranslate(targetScale, new PointF(vTranslateXEnd, vTranslateYEnd));
// Fit the end translation into bounds
fitToBounds(true, satEnd);
// Adjust the position of the focus point at end so image will be in bounds
anim.vFocusEnd = new PointF(
vFocus.x + (satEnd.translate.x - vTranslateXEnd),
vFocus.y + (satEnd.translate.y - vTranslateYEnd)
);
}
invalidate();
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#20000000"
android:endColor="@android:color/transparent"
android:angle="90" >
</gradient>
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#20000000"
android:endColor="@android:color/transparent"
android:angle="270" >
</gradient>
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Drop Shadow Stack -->
<!-- Background -->
<item>
<shape>
<solid android:color="#e5e5e5" />
</shape>
</item>
</layer-list>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#30555555"/>
<corners android:radius="2dp" />
</shape>
</item>
<item
android:left="0dp"
android:right="0dp"
android:top="0dp"
android:bottom="2dp">
<shape android:shape="rectangle">
<solid android:color="#FFFFFF"/>
<corners android:radius="2dp" />
</shape>
</item>
</layer-list>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Drop Shadow Stack -->
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#00999999" />
</shape>
</item>
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#10999999" />
</shape>
</item>
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#20999999" />
</shape>
</item>
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#10999999" />
</shape>
</item>
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#00999999" />
</shape>
</item>
<!-- Background -->
<item>
<shape>
<solid android:color="@android:color/transparent" />
</shape>
</item>
</layer-list>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Drop Shadow Stack -->
<!-- <item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#00999999" />
<corners android:radius="2dp" />
</shape>
</item>
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#10999999" />
<corners android:radius="2dp" />
</shape>
</item>
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#20999999" />
<corners android:radius="2dp" />
</shape>
</item>
<item>
<shape>
<padding android:top="0dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#30999999" />
<corners android:radius="2dp" />
</shape>
</item>
<item>
<shape>
<padding android:top="0dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#20999999" />
<corners android:radius="2dp" />
</shape>
</item>-->
<!-- Background -->
<item>
<shape>
<solid android:color="#F0FFFFFF" />
<!-- <corners android:radius="2dp" />-->
</shape>
</item>
</layer-list>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Drop Shadow Stack -->
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#00999999" />
<corners android:radius="5dp" />
</shape>
</item>
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#00999999" />
<corners android:radius="5dp" />
</shape>
</item>
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#00999999" />
<corners android:radius="5dp" />
</shape>
</item>
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#00999999" />
<corners android:radius="5dp" />
</shape>
</item>
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="#00999999" />
<corners android:radius="5dp" />
</shape>
</item>
<!-- Background -->
<item>
<shape>
<gradient
android:type="linear"
android:centerX="6%"
android:startColor="#FFe4e4e4"
android:centerColor="#FFf6f6f6"
android:endColor="#FFffffff"
android:angle="90"/>
<corners android:radius="5dp" />
</shape>
</item>
</layer-list>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<gradient
android:type="linear"
android:centerX="20%"
android:startColor="#66000000"
android:centerColor="#22000000"
android:endColor="#00f6f6f6"
android:angle="270"/>
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<gradient
android:type="linear"
android:centerX="2%"
android:startColor="#FFe4e4e4"
android:centerColor="#FFf6f6f6"
android:endColor="#FFffffff"
android:angle="90"/>
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<gradient
android:type="linear"
android:centerX="60%"
android:startColor="#00000000"
android:centerColor="#19000000"
android:endColor="#FF000000"
android:angle="270"/>
</shape>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:map="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="app.insti.fragment.MapFragment">
android:layout_height="match_parent">
<fragment
android:id="@+id/viewMap"
android:name="com.google.android.gms.maps.SupportMapFragment"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
map:cameraZoom="18" />
<!--TODO: Change colours-->
<android.support.design.widget.FloatingActionButton
android:id="@+id/location_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="48dp"
android:layout_marginEnd="@dimen/fab_margin"
android:src="@drawable/ic_my_location_black_24dp" />
android:orientation="vertical"
android:focusable="true"
android:focusableInTouchMode="true">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary">
<EditText
android:id="@+id/search"
android:layout_width="fill_parent"
android:layout_height="48dp"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:background="@null"
android:dropDownHeight="0dp"
android:fontFamily="sans-serif-light"
android:hint="Search"
android:imeOptions="actionSearch"
android:inputType="textNoSuggestions"
android:paddingBottom="8dp"
android:paddingLeft="8dp"
android:paddingRight="50dp"
android:paddingTop="8dp"
android:selectAllOnFocus="true"
android:singleLine="true"
android:textColor="@color/primaryTextColor"
android:textColorHint="@color/primaryTextColor"/>
<TextView
android:id="@+id/settings_title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:fontFamily="sans-serif-light"
android:paddingLeft="8dp"
android:paddingRight="76dp"
android:text="Settings"
android:visibility="gone" />
<ImageButton
android:id="@+id/index_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentRight="true"
android:background="@android:color/transparent"
android:contentDescription="index"
android:cropToPadding="true"
android:padding="12dp"
android:scaleType="fitXY"
android:src="@drawable/dept_menu" />
<ImageButton
android:id="@+id/map_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentRight="true"
android:background="@android:color/transparent"
android:contentDescription="map"
android:cropToPadding="true"
android:padding="12dp"
android:scaleType="fitXY"
android:src="@drawable/dept_menu_off"
android:visibility="gone" />
<ImageButton
android:id="@+id/remove_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentRight="true"
android:background="@android:color/transparent"
android:contentDescription="remove"
android:padding="8dp"
android:src="@drawable/ic_action_remove"
android:visibility="gone" />
</android.support.v7.widget.Toolbar>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray"
tools:context="com.mrane.campusmap.MainActivity"
tools:ignore="MergeRootFrame"
android:focusable="true"
android:focusableInTouchMode="true">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"></LinearLayout>
<com.mrane.navigation.SlidingUpPanelLayout xmlns:sothree="http://schemas.android.com/apk/res-auto"
android:id="@+id/sliding_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom">
<!-- MAIN CONTENT -->
<com.mrane.zoomview.CampusMapView
android:id="@+id/campusMapView"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:paddingTop="48dp" />
<!-- SLIDING LAYOUT -->
<include layout="@layout/map_card_layout" />
</com.mrane.navigation.SlidingUpPanelLayout>
<RelativeLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_alignParentTop="true"
android:orientation="vertical"></RelativeLayout>
</RelativeLayout>
</android.support.v4.widget.DrawerLayout>
</LinearLayout>
<RelativeLayout
android:id="@+id/loadingPanel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true" />
</RelativeLayout>
</FrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/place_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true" />
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dragView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#eeeeee"
android:clickable="true"
android:focusable="false"
android:orientation="vertical"
android:visibility="gone">
<LinearLayout
android:id="@+id/new_small_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/small_card_bg"
android:orientation="horizontal" >
<ImageView
android:id="@+id/place_color"
android:layout_width="12dp"
android:layout_height="fill_parent" />
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/hidden_card_height"
android:paddingBottom="8dp" >
<ImageButton
android:id="@+id/add_marker_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentTop="true"
android:layout_marginRight="0dp"
android:layout_marginTop="4dp"
android:layout_alignParentRight="true"
android:background="@android:color/transparent"
android:contentDescription="Search"
android:cropToPadding="true"
android:padding="8dp"
android:scaleType="fitXY"
android:src="@drawable/lock_all_off" />
<TextView
android:id="@+id/place_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@id/add_marker_icon"
android:ellipsize="end"
android:fontFamily="sans_serif"
android:maxLines="1"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp"
android:textSize="@dimen/place_name_text_size" >
</TextView>
<TextView
android:id="@+id/place_sub_head"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@id/place_name"
android:layout_toLeftOf="@id/add_marker_icon"
android:fontFamily="sans_serif_light"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:text=""
android:textColor="@color/secondaryTextColor"
android:textSize="@dimen/place_sub_head_text_size" />
</RelativeLayout>
</LinearLayout>
<RelativeLayout
android:id="@+id/new_expand_container"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:background="@color/list_item_gray_even" >
<com.mrane.navigation.EndDetectScrollView
android:id="@+id/new_expanded_place_card_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:id="@+id/expanded_place_card"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#e5e5e5"
android:orientation="horizontal" >
</LinearLayout>
<LinearLayout
android:id="@+id/other_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:orientation="vertical" >
</LinearLayout>
</RelativeLayout>
</com.mrane.navigation.EndDetectScrollView>
<RelativeLayout
android:id="@+id/color_strip"
android:layout_width="12dp"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" >
<View
android:id="@+id/place_group_color"
android:layout_width="12dp"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="0dp"
android:alpha="0.5"
android:paddingLeft="0dp" />
<View
android:layout_width="12dp"
android:layout_height="@dimen/expanded_card_height"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="0dp"
android:alpha="0.5"
android:background="@color/transparent_black"
android:paddingLeft="0dp" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_alignParentTop="true"
android:background="@drawable/shadow_gradient" />
</RelativeLayout>
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/child_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:minHeight="56dp"
android:textSize="18dp"
android:fontFamily="sans-serif-light" />
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#e0e0e0" >
<LinearLayout
android:id="@+id/header_layout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:orientation="horizontal">
<ImageView
android:id="@+id/arrow_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="8dp"
android:cropToPadding="true"
android:scaleType="centerInside" />
<TextView
android:id="@+id/list_header"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="left|center_vertical"
android:paddingLeft="8dp" />
</LinearLayout>
<ListView
android:id="@+id/child_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/header_layout"
android:paddingLeft="48dp"
android:background="#e5e5e5"></ListView>
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="vertical"
android:background="#F5F5F5" >
<ImageView
android:id="@+id/group_color"
android:layout_width="12dp"
android:layout_height="56dp"
android:layout_alignParentLeft="true"
android:paddingLeft="0dp"
android:layout_marginLeft="0dp"
android:cropToPadding="true"/>
<ImageView
android:id="@+id/icon_expand"
android:layout_width="32dp"
android:layout_height="56dp"
android:layout_toRightOf="@id/group_color"
android:cropToPadding="true"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:contentDescription="list item"
android:visibility="visible" />
<TextView
android:id="@+id/lblListHeader"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/icon_expand"
android:padding="8dp"
android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentTop="true" />
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ExpandableListView
android:id="@+id/index_list"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:divider="@null"
android:paddingLeft="0dp"
android:paddingRight="0dp"
android:groupIndicator="@null"
android:background="@android:color/transparent"
android:transcriptMode="disabled" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/suggestion_list"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:background="@drawable/listview_background" >
</ListView>
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:orientation="horizontal"
android:background="#F0E5E5E5">
<RelativeLayout
android:layout_width="12dp"
android:layout_height="fill_parent">
<View
android:id="@+id/item_group_color"
android:layout_width="12dp"
android:layout_height="fill_parent"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:paddingLeft="0dp"
android:layout_marginLeft="0dp"/>
<View
android:layout_width="12dp"
android:layout_height="fill_parent"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:paddingLeft="0dp"
android:layout_marginLeft="0dp" />
</RelativeLayout>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:paddingRight="8dp">
<View
android:layout_width="fill_parent"
android:layout_height="1dp"
android:padding="8dp"
android:layout_marginLeft="40dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true" />
<View
android:layout_width="fill_parent"
android:layout_height="1dp"
android:padding="8dp"
android:layout_marginLeft="40dp"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true" />
<TextView
android:id="@+id/lblListItem"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:padding="8dp"
android:fontFamily="sans-serif-light"
android:layout_marginLeft="48dp" />
</RelativeLayout>
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="12dp" >
<TextView
android:id="@+id/desc_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:paddingBottom="0dp"
android:textSize="20sp"
android:text="Description"/>
<TextView
android:id="@+id/desc_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/desc_header"
android:lineSpacingExtra="3dp"
android:padding="8dp"
android:textSize="18sp"
android:paddingTop="0dp"
android:textIsSelectable="true"/>
<View
android:layout_below="@id/desc_content"
android:layout_width="match_parent"
android:layout_height="96dp"/>
<View
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:background="@color/colorSecondary"/>
<View
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:background="@color/colorSecondary"/>
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:fontFamily="sans-serif-light" >
</TextView>
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2014 David Morrissey
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.
-->
<resources>
<declare-styleable name="SubsamplingScaleImageView">
<attr name="assetName" format="string" />
<attr name="panEnabled" format="boolean" />
<attr name="zoomEnabled" format="boolean" />
</declare-styleable>
<declare-styleable name="SlidingUpPanelLayout">
<attr name="panelHeight" format="dimension" />
<attr name="shadowHeight" format="dimension" />
<attr name="paralaxOffset" format="dimension" />
<attr name="fadeColor" format="color" />
<attr name="flingVelocity" format="integer" />
<attr name="dragView" format="reference" />
<attr name="overlay" format="boolean" />
<attr name="anchorPoint" format="float" />
<attr name="initialState" format="enum">
<enum name="expanded" value="0" />
<enum name="collapsed" value="1" />
<enum name="anchored" value="2" />
<enum name="hidden" value="3" />
</attr>
</declare-styleable>
</resources>
\ No newline at end of file
......@@ -14,4 +14,9 @@
<color name="colorCalendarWeek">#000000</color>
<color name="colorGray">#757575</color>
<color name="colorWhite">#FFFFFF</color>
<!-- Map -->
<item name="transparent_black" type="color">#20000000</item>
<item name="list_item_gray_even" type="color">#ffe6e6e6</item>
<item name="list_item_gray_odd" type="color">#ffececec</item>
</resources>
......@@ -7,4 +7,15 @@
<dimen name="fab_margin">16dp</dimen>
<dimen name="quick_links_size">18sp</dimen>
<dimen name="links_margin_start">8dp</dimen>
<!-- Map -->
<dimen name="card_height">360dp</dimen>
<dimen name="hidden_card_height">80dp</dimen>
<dimen name="expanded_card_height">280dp</dimen>
<dimen name="place_name_text_size">20sp</dimen>
<dimen name="place_sub_head_text_size">16sp</dimen>
<dimen name="index_header_text_size">20sp</dimen>
<dimen name="index_item_text_size">16sp</dimen>
<dimen name="list_item_text_size">16sp</dimen>
<dimen name="searchbar_text_size">16sp</dimen>
</resources>
......@@ -36,4 +36,7 @@
<item>Tansa House</item>
<item>QIP</item>
</string-array>
<string name="drawer_open">Open the drawer</string>
<string name="drawer_close">Close the drawer</string>
</resources>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment