How to change GoogleMap V2 camera center? - android

I am trying to change Google map V2 camera center position to be lower to the center of the map view.
So far, I've partially succeeded to achive this with the help of a few solutions such as this
This solution did the job but the actual camera position still remains in the center of the map!
It "pushed" the target location down but the camera still "looking" on the center of the MapView.
The problem with this approach comes when you want do animations and map movement such in navigation.
You need to calculate the offset for every location you get from the GPS and when you do hard turns the offset is not the same as Camera animation route is not in my control .
For instance, if I move tward some direction on foot and then suddenly I turn 180 degrees, the animation will push my target location until the animation complete and then it will look ok, thats because the the map rotation and animation is always around the cam position which is always in the center of the view.
Fo instance, in Google Map Navigation - The Nav marker is at the bottom of the map, and it seems to rotate and animate around that position and not around the view center.
Here is a screen shot the result i've got from above solutions:
What I want is that the camera position will be in my target position.
Google map navigation:

You should use Map Padding, namely the setPadding(int left, int top, int right, int bottom) method:
...
public void onMapReady(GoogleMap googleMap) {
mGoogleMap = googleMap;
mGoogleMap.setPadding(0, dHeight, 0, 0);
...
}
...
where dHeight = Target_pos.y - cam_pos.y. And you should move compass button, like in this answer of Vignon with some changes for drawing compass outside of clipping region:
try {
final ViewGroup parent = (ViewGroup) mMapFragment.getView().findViewWithTag("GoogleMapMyLocationButton").getParent();
parent.post(new Runnable() {
#Override
public void run() {
try {
Resources r = getResources();
//convert our dp margin into pixels
int marginPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, r.getDisplayMetrics());
// Get the map compass view
View mapCompass = parent.getChildAt(4);
View v = mapCompass;
while (v.getParent() != null && v.getParent() instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) v.getParent();
viewGroup.setClipChildren(false);
viewGroup.setClipToPadding(false);
v = viewGroup;
}
// create layoutParams, giving it our wanted width and height(important, by default the width is "match parent")
RelativeLayout.LayoutParams rlp = new RelativeLayout.LayoutParams(mapCompass.getHeight(),mapCompass.getHeight());
// position on top right
rlp.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
rlp.addRule(RelativeLayout.ALIGN_PARENT_TOP, 0);
rlp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, 0);
rlp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 0);
//give compass margin
rlp.setMargins(marginPixels, dHeight, marginPixels, marginPixels);
mapCompass.setLayoutParams(rlp);
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
} catch (Exception ex) {
ex.printStackTrace();
}
Also you can try that dirty solution (see the picture)
is to scale height of MapFragment (MapView) to dHeight more then visible area (screen height), where:
dHeight = (int) (k * MapFragment.height)
where k = Target_pos.y / cam_pos.y . In that case map center moved exactly to your desired point (but bottom (white on picture) part of map will not be visible). Also you should move up zoom controls, because by default it also at the bottom of map and will not be visible too. You can do this like in this answer. But remember, that as per the Google Maps APIs Terms of Service, your application must not remove or obscure the Google logo or copyright notices.
Seems the right solution is to create custom view, which extends MapFragment or MapView, e.g. like in that answer and do all your every location offset calculate magic inside it.

Finally I've came to the solution!!
The padding solution is good for a static map but when you want to do continuance animation on the map like in navigation, the padding causes to un expected movement in turns, especially when the map tilts and zoomed!!
The MapView size solution is the right choice here as it simply push the center of the map.
The problem with that approach is that it also pushes down the zoom control and most important the Google logo which must be visible.
So, you have to explicitly set the the zoom control and Google logo bottom margin back so they will be visible again! This without putting any padding to the map view itself.
Alternatively you can set the controls and the logo position to different corners if it's ok for your need, I've decided just to set their margins.
Here is my code:
final ViewTreeObserver observer = mapView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
mainParentLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int parentHeight = ((ViewGroup)mapView.getParent()).getHeight() - toolbar.getHeight();
int mapHeight = parentHeight + parentHeight/2;
mapView.getLayoutParams().height = mapHeight;
int margin = mapHeight - parentHeight + 2*((int)(getResources().getDimension(R.dimen.small_image) + getResources().getDimension(R.dimen.small_pargin)));
Log.i(TAG, "onGlobalLayout: parent height: " + parentHeight + ", map new height: " + mapHeight + ", margin: " + margin);
setMapControlsMargin(margin);
}
});
This code, waits for the map view to be shown, then it calculate the map parent view height (in my case a ConstraintLayout) then it set the map height to be parentHeight * 1.5. This will push the camera center down!
Then, set the margin the Google logo and zoom controls.
private void setMapControlsMargin(final int margin){
try {
//Find the Zoom controls parent
final ViewGroup parent = (ViewGroup) mapView.findViewWithTag("GoogleMapMyLocationButton").getParent();
//Find the Google logo parent view group
final ViewGroup googleLogo = (ViewGroup) mapView.findViewWithTag("GoogleWatermark").getParent();
Log.i(TAG, "setMapControlsMargin: found Google logo " + (googleLogo != null));
//Set the margin for the Google logo
if (googleLogo != null) {
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) googleLogo.getLayoutParams();
layoutParams.bottomMargin = margin;
}
parent.post(() -> {
try {
View zoomControls = parent.getChildAt(2); //Zoom controls
ViewGroup.LayoutParams layoutParams1 = zoomControls.getLayoutParams();
//Read the zoom controls original margin, this will be added to the offset margin
int origBotMargin = ((RelativeLayout.LayoutParams) layoutParams1).bottomMargin;
int origRightMargin = ((RelativeLayout.LayoutParams) layoutParams1).rightMargin;
// create layoutParams, giving it our wanted width and height(important, by default the width is "match parent")
RelativeLayout.LayoutParams rlp = new RelativeLayout.LayoutParams(zoomControls.getWidth(),zoomControls.getHeight());
// position on bottom right
rlp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
rlp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
//set the new margin
rlp.setMargins(0, 0, origRightMargin, origBotMargin + margin);
zoomControls.setLayoutParams(rlp);
} catch (Exception ex) {
ex.printStackTrace();
}
});
} catch (Exception ex) {
ex.printStackTrace();
}
}
I was helped this post to move the Google logo.

Related

Maintaining x and y across screen sizes

I have an imageview with a layout on top of it which has buttons added at specific x and y coordinates. How do i make sure that everything lines up the same on different screen sizes so the buttons are always in the same position relative to the image regardless of screen, also how to maintain all the positioning if the orientation is horizontal.
I actually built a class specifically or this. You can find it here:
https://gist.github.com/nathan-fiscaletti/190a660620f6130e6a15962f59b21f22
The way you would use it is as follows:
Replace the ImageView in your layout with SizeAwareImageView.
<com.my.app.SizeAwareImageView
android:id="#+id/sizeAwareImageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/viewHeader"
app:srcCompat="#drawable/somePicture" />
You can then determine the placement of those objects and use use the following to figure out where to place them.
PointF location = sizeAwareImageView.getScaledCoordinateWithPadding(new PointF(10f, 10f));
// place the object at the location
This will keep your objects at the same location based on the drawable applied to the image view. No matter what screen it is on, the location variable will always point to the same placement on the image.
I use this to store the location of map markers in a database, they can then be used to move a pin around a static map drawable regardless of what device they are displayed on.
/**
* Update the map markers location and display it.
*
* #param mapPinLocation The new location.
*/
private void updateMarker(final PointF mapPinLocation) {
markerContainer.removeView(mapMarker);
PointF location = mapImageView
.getScaledCoordinateWithPadding(mapPinLocation, 100f);
if (getContext() != null) {
int markerSize = (int) DisplayUtil.dpToPx(25, getContext());
RelativeLayout.LayoutParams lp
= new RelativeLayout.LayoutParams(markerSize, markerSize);
// Android uses top left of the view for it's coords
// so we need to center it at it's X/Y coords.
if (location != null) {
lp.topMargin = (int) location.y - (markerSize / 2);
lp.leftMargin = (int) location.x - (markerSize / 2);
mapMarker.setLayoutParams(lp);
markerContainer.addView(mapMarker);
}
}
}
Hopefully this is actually what you were looking for.

How to focus on location on Google maps, considering a view is on top of it?

Background
Suppose I have a Google maps view, and another view on top of it, that covers a part of it, hiding some content of the map.
The problem
I need to make the "camera" of the map, to focus and have a marker on a coordinate , yet let it all be in the middle of the visible part of the map.
Something like this:
The original code was focusing on (about) the center of the entire screen, making the marker almost invisible (as the bottom view covers it).
Thing is, I can't find the proper way to set the correct value to the Y coordinate of the map itself (meaning latitude).
What I've tried
I tried, given the height of the bottom view, and the coordinate that I've put the marker on, to calculate the delta (yet of course not change the marker itself) :
final float neededZoom = 6.5f;
int bottomViewHeight = bottomView.getHeight();
LatLng posToFocusOn = ...;
final Point point = mMap.getProjection().toScreenLocation(posToFocusOn);
final float curZoom = mMap.getCameraPosition().zoom;
point.y += bottomViewHeight * curZoom / neededZoom;
posToFocusOn = mMap.getProjection().fromScreenLocation(point);
final CameraUpdate cameraPosition = CameraUpdateFactory.newCameraPosition(new Builder().target(posToFocusOn).zoom(neededZoom).build());
Sadly, this focuses way above the marker.
The question
What's wrong with what I wrote? What can I do to fix it?
ok, I've found a workaround, which I think works on all devices (tested on 3, each with a different screen resolution and size) :
I've measured how many pixels (and then converted to DP) a change of one degree has on the marker itself.
From this, I measured the height of each view, and calculated the delta needed to move the camera.
In my case, it's this way (supposing the zoom is 6.5f) :
//measured as 223 pixels on Nexus 5, which has xxhdpi, so divide by 3
final float oneDegreeInPixels = convertDpToPixels( 223.0f / 3.0f);
final float mapViewCenter = mapViewHeight / 2.0f;
final float bottomViewHeight = ...;
final float posToFocusInPixelsFromTop = (mapViewHeight - bottomViewHeight) / 2.0f ;// can optionally add the height of the view on the top area
final float deltaLatDegreesToMove = (mapViewCenter - posToFocusInPixelsFromTop) / oneDegreeInPixels;
LatLng posToFocusOn = new LatLng(latitude - deltaLatDegreesToMove, longitude);
final CameraUpdate cameraPosition = CameraUpdateFactory.newCameraPosition(new Builder().target(posToFocusOn).zoom(neededZoom).build());
And it worked.
I wonder if it can be adjusted to support any value of zoom.
Your code is almost right, but it goes above the marker because you are taking into account bottomViewHeight when computing point.y instead of bottomViewHeight/2 (When your view's size is 200px, you only need to displace the map 100px to recenter it):
point.y += (bottomViewHeight / 2) * curZoom / neededZoom;
Update:
This is a more general approach taht takes into account the map bounds and calculates a new map bounds according to the height of your bottomView. This is zoom independent.
public void recenter() {
LatLngBounds mapBounds = mMap.getProjection().getVisibleRegion().latLngBounds;
Point nothEastPoint = mMap.getProjection().toScreenLocation(mapBounds.northeast);
Point souhWestPoint = mMap.getProjection().toScreenLocation(mapBounds.southwest);
Point newNorthEast = new Point(nothEastPoint.x, nothEastPoint.y + bottomView.getHeight() / 2);
Point newSouhWestPoint = new Point(souhWestPoint.x, souhWestPoint.y + bottomView.getHeight() / 2);
LatLngBounds newBounds = LatLngBounds.builder()
.include(mMap.getProjection().fromScreenLocation(newNorthEast))
.include(mMap.getProjection().fromScreenLocation(newSouhWestPoint))
.build();
mMap.moveCamera(CameraUpdateFactory.newLatLngBounds(newBounds, 0));
}
Note that each time you call recenter() the map will move.

Animate imageView from one side of screen to the other - android

I am trying to animate an ImageView using TranslateAnimation from it's current position (0,Yscreensize/2) to the other side of the screen (Xscreensize,imageview.getY()) but I can't manage it to work. here is my code:
dart = (ImageView) findViewById(R.id.dart);
Display disp = getWindowManager().getDefaultDisplay();
Point poi = new Point();
disp.getSize(poi);
sizex = poi.x;
sizey = poi.y;
ViewTreeObserver vto = dart.getViewTreeObserver();
vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
public boolean onPreDraw() {
dart.getViewTreeObserver().removeOnPreDrawListener(this);
finalHeight = dart.getMeasuredHeight();
dart.setY(sizey / 2 - finalHeight);
return true;
}
}); // till now - got screen size and set dart imageview to Y middle.
Now when I am trying to use the animation:
TranslateAnimation animation = new TranslateAnimation(dart.getX(), sizex, dart.getY(), dart.getY());
animation.setDuration(10000);
dart.startAnimation(animation); // from current x to screen size, from currenty to currenty.
this will not work and the dart just dissappear. what can I do?
Make the variable for the animation a class variable (to ensure it won't be garbage collected to soon). In order to make the ImageView stop its movement before it leaves the screen, you need an additional class variable
float finalWidth;
Then change your OnPreDrawListener like this:
vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
{
public boolean onPreDraw()
{
dart.getViewTreeObserver().removeOnPreDrawListener(this);
float finalHeight = dart.getMeasuredHeight();
// if you want the view to stop before it leaves the screen
float finalWidth = dart.getMeasuredWidth();
animation = new TranslateAnimation(0, sizex - finalWidth, 0, 0);
animation.setDuration(10000);
// use this if you want the changes to persist
// animation.setFillAfter(true);
dart.setY(sizey / 2 - finalHeight);
return true;
}
});
I used a button to call 'startAnimation()'. The only problem when testing with an emulator is that the ImageView keeps running until it reaches the emulator window egde. So it disappears below the hardware controls region on the right side. In order to make it stop earlier, I tested with
Rect myRect = new Rect();
dart.getWindowVisibleDisplayFrame(myRect);
sizex = myRect.width() * 0.9f;
sizey = myRect.height();
which almost did the trick. It's just an approximation, all right, but getting the exact dimensions of the display - taking into account paddings or insets - seems to be difficult. At least below API 20.
Hope this helps anyway :)

Layout flicker when dragging view in its MotionEvent.ACTION_MOVE android

Here is a picture of what I'm trying to do:
So, I want to resize a single cell while dragging resize anchors (black quads) which are ImageViews. To do this I attached custom onTouchListener to them that does next in MotionEvent.ACTION_MOVE:
calculate drag offset
set cell height/width based on this offset
reposition anchor point by changing it's layout params
The outcome of this is that the cell resizes but there is some king of flicker, more like shaking left/right or up/down, by some small offset.
My guess is that the problem comes when it catches move event, then I manualy change position of anchor and then, when it catches move event again it doesn't handle that change well...or something
I have an idea to put invisible ImageViews under each anchor and do resize based on their movement but do not move that anchor while draging. Then when I relese it, it lines up with coresponding visible anchor. But this is more hacking than solution :)
And finally, does anubody know why is this happening?
EDIT:
Here is the code where I'm handlign move event:
float dragY = event.getRawY() - resizePreviousPositionY;
LinearLayout.LayoutParams componentParams = (LinearLayout.LayoutParams)selectedComponent.layout.getLayoutParams();
LinearLayout parrent = (LinearLayout)selectedComponent.layout.getParent();
View topSibling = parrent.getChildAt(parrent.indexOfChild(selectedComponent.layout) - 1);
LinearLayout.LayoutParams topSiblingParams = (LinearLayout.LayoutParams)topSibling.getLayoutParams();
if(dragY > 0 && selectedComponent.layout.getHeight() > 100 ||
dragY < 0 && topSibling.getHeight() > 100)
{
componentParams.height = selectedComponent.layout.getHeight() - (int)dragY;
topSiblingParams.height = topSibling.getHeight() + (int)dragY;
//bottomSiblingParams.height = bottomSibling.getHeight();
selectedComponent.layout.setLayoutParams(componentParams);
repositionResizeAnchors(selectedComponent);
}
resizePreviousPositionY = event.getRawY();
and here is where I reposition it:
if(((LinearLayout)viewHolder.layout.getParent()).getOrientation() == LinearLayout.VERTICAL)
{
leftMargin = ((LinearLayout)viewHolder.layout.getParent()).getLeft() +
viewHolder.layout.getLeft() + viewHolder.layout.getWidth()/2;
}
else
{
leftMargin = viewHolder.layout.getLeft() + viewHolder.layout.getWidth()/2;
}
topMargin = viewHolder.layout.getTop();
params = (RelativeLayout.LayoutParams)resizeTop.getLayoutParams();
params.leftMargin = leftMargin - dpToPx(resizeAnchorRadius/2);
params.topMargin = topMargin - dpToPx(resizeAnchorRadius/2);
resizeTop.setLayoutParams(params);

Implementing a multicolumn ListView with independent Row-heights

I would like to create a list of about 200 ImageViews (random heights) with the following layout in a 'collage' fashion:
Normally I would do this in a ListView for the peformance gained by using Adapters but since i want the images to be displayed in columns, and with different height (See picture Example ) depending on the pictures, I cannot use a single listview for this purpose.
I have tried implementing this layout with:
Three ListViews with synchronized scrolling = Slow
Single ListView with each row containing three images = Not allowing different heights
GridView = Not allowing different heights
GridLayout = Difficult to implement different heights programmatically. Because of no adapter, OutOfMemoryErrors are common
FlowLayout = Because of no adapter, OutOfMemoryErrors are common
ScrollView with three Vertical LinearLayouts = Best solution so far, but OutOfMemoryErrors are common
I have ended up using three LinearLayouts in a ScrollView, but this is far from optimal. I would rather use something with an Adapter.
EDIT
I have been looking at the StaggeredGridView, as in a response below, but I find it quite buggy. Are there any implementations of this that are more stable?
I think I have a working solution for you.
The main files mentioned here are also on PasteBin at http://pastebin.com/u/morganbelford
I basically implemented a simplified equivalent of the github project mentioned, https://github.com/maurycyw/StaggeredGridView, using a set of excellent LoopJ SmartImageViews.
My solution is not nearly as generic and flexible as the StaggeredGridView, but seems to work well, and quickly. One big difference functionally is that we layout the images always just left to right, then left to right again. We don't try to put the next image in the shortest column. This makes the bottom of the view a little more uneven, but generates less shifting around during initial load from the web.
There are three main classes, a custom StagScrollView, which contains a custom StagLayout (subclassed FrameLayout), which manages a set of ImageInfo data objects.
Here is our layout, stag_layout.xml (the 1000dp initial height is irrelevant, since it will get recomputed in code based on the image sizes):
// stag_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<com.morganbelford.stackoverflowtest.pinterest.StagScrollView xmlns:a="http://schemas.android.com/apk/res/android"
a:id="#+id/scroller"
a:layout_width="match_parent"
a:layout_height="match_parent" >
<com.morganbelford.stackoverflowtest.pinterest.StagLayout
a:id="#+id/frame"
a:layout_width="match_parent"
a:layout_height="1000dp"
a:background="#drawable/pinterest_bg" >
</com.morganbelford.stackoverflowtest.pinterest.StagLayout>
</com.morganbelford.stackoverflowtest.pinterest.StagScrollView>
Here is our main Activity's onCreate, which uses the layout. The StagActivity just basically tells the StagLayout what urls to use, what the margin should be between each image, and how many columns there are. For more modularity, we could have passed these params to the StagScrollView (which contains the StagLayout, but the the scroll view would have just had to pass them down the layout anyway):
// StagActivity.onCreate
setContentView(R.layout.stag_layout);
StagLayout container = (StagLayout) findViewById(R.id.frame);
DisplayMetrics metrics = new DisplayMetrics();
((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(metrics);
float fScale = metrics.density;
String[] testUrls = new String[] {
"http://www.westlord.com/wp-content/uploads/2010/10/French-Bulldog-Puppy-242x300.jpg",
"http://upload.wikimedia.org/wikipedia/en/b/b0/Cream_french_bulldog.jpg",
"http://bulldogbreeds.com/breeders/pics/french_bulldog_64368.jpg",
"http://www.drsfostersmith.com/images/articles/a-french-bulldog.jpg",
"http://2.bp.blogspot.com/-ui2p5Z_DJIs/Tgdo09JKDbI/AAAAAAAAAQ8/aoTdw2m_bSc/s1600/Lilly+%25281%2529.jpg",
"http://www.dogbreedinfo.com/images14/FrenchBulldog7.jpg",
"http://dogsbreed.net/wp-content/uploads/2011/03/french-bulldog.jpg",
"http://www.theflowerexpert.com/media/images/giftflowers/flowersandoccassions/valentinesdayflowers/sea-of-flowers.jpg.pagespeed.ce.BN9Gn4lM_r.jpg",
"http://img4-2.sunset.timeinc.net/i/2008/12/image-adds-1217/alcatraz-flowers-galliardia-m.jpg?300:300",
"http://images6.fanpop.com/image/photos/32600000/bt-jpgcarnation-jpgFlower-jpgred-rose-flow-flowers-32600653-1536-1020.jpg",
"http://the-bistro.dk/wp-content/uploads/2011/07/Bird-of-Paradise.jpg",
"http://2.bp.blogspot.com/_SG-mtHOcpiQ/TNwNO1DBCcI/AAAAAAAAALw/7Hrg5FogwfU/s1600/birds-of-paradise.jpg",
"http://wac.450f.edgecastcdn.net/80450F/screencrush.com/files/2013/01/get-back-to-portlandia-tout.jpg",
"http://3.bp.blogspot.com/-bVeFyAAgBVQ/T80r3BSAVZI/AAAAAAAABmc/JYy8Hxgl8_Q/s1600/portlandia.jpg",
"http://media.oregonlive.com/ent_impact_tvfilm/photo/portlandia-season2jpg-7d0c21a9cb904f54.jpg",
"https://twimg0-a.akamaihd.net/profile_images/1776615163/PortlandiaTV_04.jpg",
"http://getvideoartwork.com/gallery/main.php?g2_view=core.DownloadItem&g2_itemId=85796&g2_serialNumber=1",
"http://static.tvtome.com/images/genie_images/story/2011_usa/p/portlandia_foodcarts.jpg",
"http://imgc.classistatic.com/cps/poc/130104/376r1/8728dl1_27.jpeg",
};
container.setUrls(testUrls, fScale * 10, 3); // pass in pixels for margin, rather than dips
Before we get to the meat of the solution, here is our simple StagScrollView subclass. His only special behavior is to tell his main child (our StagLayout) which the currently visible area is, so that he can efficiently use the smallest possible number of realized subviews.
// StagScrollView
StagLayout _frame;
#Override
protected void onFinishInflate() {
super.onFinishInflate();
_frame = (StagLayout) findViewById(R.id.frame);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (oldh == 0)
_frame.setVisibleArea(0, h);
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
_frame.setVisibleArea(t, t + getHeight());
}
Here then is the most important class StagLayout.
First, setUrls sets up our data structures.
public void setUrls(String[] urls, float pxMargin, int cCols)
{
_pxMargin = pxMargin;
_cCols = cCols;
_cMaxCachedViews = 2 * cCols;
_infos = new ArrayList<ImageInfo>(urls.length); // should be urls.length
for (int i = 0; i < 200; i++) // should be urls.length IRL, but this is a quick way to get more images, by using repeats
{
final String sUrl = urls[i % urls.length]; // could just be urls[i] IRL
_infos.add(new ImageInfo(sUrl, new OnClickListener() {
#Override
public void onClick(View v) {
Log.d("StagLayout", String.format("Image clicked: url == %s", sUrl));
}
}));
}
_activeInfos = new HashSet<ImageInfo>(_infos.size());
_cachedViews = new ArrayList<SmartImageView>(_cMaxCachedViews);
requestLayout(); // perform initial layout
}
Our main data structure is ImageInfo. It is a kind of lightweight placeholder that allows us to keep track of where each image is going to be displayed, when it needs to be. When we layout our child views, we will use the information in the ImageInfo to figure out where to put the actual view. A good way to think about ImageInfo is as a "virtual image view".
See comments inline for details.
public class ImageInfo {
private String _sUrl;
// these rects are in float dips
private RectF _rLoaded; // real size of the corresponding loaded SmartImageView
private RectF _rDefault; // lame default rect in case we don't have anything better to go on
private RectF _rLayout; // rect that our parent tells us to use -- this corresponds to a real View's layout rect as specified when parent ViewGroup calls child.layout(l,t,r,b)
private SmartImageView _vw;
private View.OnClickListener _clickListener;
public ImageInfo(String sUrl, View.OnClickListener clickListener) {
_rDefault = new RectF(0, 0, 100, 100);
_sUrl = sUrl;
_rLayout = new RectF();
_clickListener = clickListener;
}
// Bounds will be called by the StagLayout when it is laying out views.
// We want to return the most accurate bounds we can.
public RectF bounds() {
// if there is not yet a 'real' bounds (from a loaded SmartImageView), try to get one
if (_rLoaded == null && _vw != null) {
int h = _vw.getMeasuredHeight();
int w = _vw.getMeasuredWidth();
// if the SmartImageView thinks it knows how big it wants to be, then ok
if (h > 0 && w > 0) {
_rLoaded = new RectF(0, 0, w, h);
}
}
if (_rLoaded != null)
return _rLoaded;
// if we have not yet gotten a real bounds from the SmartImageView, just use this lame rect
return _rDefault;
}
// Reuse our layout rect -- this gets called a lot
public void setLayoutBounds(float left, float top, float right, float bottom) {
_rLayout.top = top;
_rLayout.left = left;
_rLayout.right = right;
_rLayout.bottom = bottom;
}
public RectF layoutBounds() {
return _rLayout;
}
public SmartImageView view() {
return _vw;
}
// This is called during layout to attach or detach a real view
public void setView(SmartImageView vw)
{
if (vw == null && _vw != null)
{
// if detaching, tell view it has no url, or handlers -- this prepares it for reuse or disposal
_vw.setImage(null, (SmartImageTask.OnCompleteListener)null);
_vw.setOnClickListener(null);
}
_vw = vw;
if (_vw != null)
{
// We are attaching a view (new or re-used), so tell it its url and attach handlers.
// We need to set this OnCompleteListener so we know when to ask the SmartImageView how big it really is
_vw.setImageUrl(_sUrl, R.drawable.default_image, new SmartImageTask.OnCompleteListener() {
final private View vw = _vw;
#Override
public void onComplete() {
vw.measure(MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED));
int h = vw.getMeasuredHeight();
int w = vw.getMeasuredWidth();
_rLoaded = new RectF(0, 0, w, h);
Log.d("ImageInfo", String.format("Settings loaded size onComplete %d x %d for %s", w, h, _sUrl));
}
});
_vw.setOnClickListener(_clickListener);
}
}
// Simple way to answer the question, "based on where I have laid you out, are you visible"
public boolean overlaps(float top, float bottom) {
if (_rLayout.bottom < top)
return false;
if (_rLayout.top > bottom)
return false;
return true;
}
}
The rest of the magic happens in StagLayout's onMeasure and onLayout.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
// Measure each real view that is currently realized. Initially there are none of these
for (ImageInfo info : _activeInfos)
{
View v = info.view();
v.measure(MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED));
}
// This arranges all of the imageinfos every time, and sets _maxBottom
//
computeImageInfo(width);
setMeasuredDimension(width, (int)_maxBottom);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// This figures out what real SmartImageViews we need, creates new ones, re-uses old ones, etc.
// After this call _activeInfos is correct -- the list of ImageInfos that are currently attached to real SmartImageViews
setupSubviews();
for (ImageInfo info : _activeInfos)
{
// Note: The layoutBounds of each info is actually computed in onMeasure
RectF rBounds = info.layoutBounds();
// Tell the real view where it should be
info.view().layout((int)rBounds.left, (int)rBounds.top, (int)rBounds.right, (int)rBounds.bottom);
}
}
Ok, now let's see how we actually arrange all the ImageInfos.
private void computeImageInfo(float width)
{
float dxMargin = _pxMargin;
float dyMargin = _pxMargin;
float left = 0;
float tops[] = new float[_cCols]; // start at 0
float widthCol = (int)((width - (_cCols + 1) * dxMargin) / _cCols);
_maxBottom = 0;
// layout the images -- set their layoutrect based on our current location and their bounds
for (int i = 0; i < _infos.size(); i++)
{
int iCol = i % _cCols;
// new row
if (iCol == 0)
{
left = dxMargin;
for (int j = 0; j < _cCols; j++)
tops[j] += dyMargin;
}
ImageInfo info = _infos.get(i);
RectF bounds = info.bounds();
float scale = widthCol / bounds.width(); // up or down, for now, it does not matter
float layoutHeight = bounds.height() * scale;
float top = tops[iCol];
float bottom = top + layoutHeight;
info.setLayoutBounds(left, top, left + widthCol, bottom);
if (bottom > _maxBottom)
_maxBottom = bottom;
left += widthCol + dxMargin;
tops[iCol] += layoutHeight;
}
// TODO Optimization: build indexes of tops and bottoms
// Exercise for reader
_maxBottom += dyMargin;
}
And, now let's see how we create, resuse and dispose of real SmartImageViews during onLayout.
private void setupSubviews()
{
// We need to compute new set of active views
// TODO Optimize enumeration using indexes of tops and bottoms
// NeededInfos will be set of currently visible ImageInfos
HashSet<ImageInfo> neededInfos = new HashSet<ImageInfo>(_infos.size());
// NewInfos will be subset that are not currently assigned real views
HashSet<ImageInfo> newInfos = new HashSet<ImageInfo>(_infos.size());
for (ImageInfo info : _infos)
{
if (info.overlaps(_viewportTop, _viewportBottom))
{
neededInfos.add(info);
if (info.view() == null)
newInfos.add(info);
}
}
// So now we have the active ones. Lets get any we need to deactivate.
// Start with a copy of the _activeInfos from last time
HashSet<ImageInfo> unneededInfos = new HashSet<ImageInfo>(_activeInfos);
// And remove all the ones we need now, leaving ones we don't need any more
unneededInfos.removeAll(neededInfos);
// Detach all the views from these guys, and possibly reuse them
ArrayList<SmartImageView> unneededViews = new ArrayList<SmartImageView>(unneededInfos.size());
for (ImageInfo info : unneededInfos)
{
SmartImageView vw = info.view();
unneededViews.add(vw);
info.setView(null); // at this point view is still a child of parent
}
// So now we try to reuse the views, and create new ones if needed
for (ImageInfo info : newInfos)
{
SmartImageView vw = null;
if (unneededViews.size() > 0)
{
vw = unneededViews.remove(0); // grab one of these -- these are still children and so dont need to be added to parent
}
else if (_cachedViews.size() > 0)
{
vw = _cachedViews.remove(0); // else grab a cached one and re-add to parent
addViewInLayout(vw, -1, new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
}
else
{
vw = new SmartImageView(getContext()); // create a whole new one
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
addViewInLayout(vw, -1, lp); // and add to parent
}
info.setView(vw); // info should also set its data
}
// At this point, detach any unneeded views and add to our cache, up to limit
for (SmartImageView vw : unneededViews)
{
// tell view to cancel
removeViewInLayout(vw); // always remove from parent
if (_cachedViews.size() < _cMaxCachedViews)
_cachedViews.add(vw);
}
// Record the active ones for next time around
_activeInfos = neededInfos;
}
Remember that _viewportTop and _viewportBottom are set every time the user scrolls.
// called on every scroll by parent StagScrollView
public void setVisibleArea(int top, int bottom) {
_viewportTop = top;
_viewportBottom = bottom;
//fixup views
if (getWidth() == 0) // if we have never been measured, dont do this - it will happen in first layout shortly
return;
requestLayout();
}
You can have a look at https://github.com/maurycyw/StaggeredGridView
I have not worked with it personally, but you could atleast steal some concepts.
Create a list view in a layout.
Create another layout with same background as that of list view background layout with three Image Views (next to each other ie to the right of each other) with their properties set to Wrap_Content horizontally and the whole Views properties in which image views are put to Wrap_Content.
Inflate the layout in the getview() method of listview adapter. In this you need to set 3 set of images in Image Views of the inflated Layout.
Hope this helps!
I guess it can be implemented with three independent list view, only thing which you have to do it to inflate layout for imageview and add it to listview.
use following as layout parameters during inflation.
Layout Width : match_parent
layout Height: wrap_content
you can assign layout weight as .3 for all the three list view with layout_width as 0dp and height as fill_parent.
hope this helps.
Can't you use your current solution wrapped in a custom list ?
in getView method for each row inflate your existing solution (checking converview ofcourse)
i.e. ScrollView with three Vertical LinearLayouts.
Do you know why the 3 List View solution was slow?
How many different sizes are in each column? I think that for the recycling of views to be efficient, you would want to create a view type for each size of image, and then make sure that you use getItemViewType, to be sure that you're recycling the correct type of view. Otherwise, you will not get much benefit from the recycling. You would want to be able to just reset the source for the image view.

Categories

Resources