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.
Related
I'm developing a custom keyboard using Xamarin.
My keyboard view has an overridden OnDraw() for both the view container itself and it's child key views. I'm also using SetWillNotDraw(false) appropriately for each view. It currently works beautifully in 5.0.1 on my Nexus 10 tablet.
In Android 6.0.1, on a Nexus 6 and a Nexus 6P, the keyboard view correctly draws itself (just a background color). The child key views however are never drawn, even if I iterate through the view hierarchy and force an invalidate on each one. This seems to be specific to Marshmallow.
I don't know if there's something new I need to account for in this version of Android or if I'm encountering a bug.
Any help or suggestions are welcome.
Code:
KeyboardView
KeyView
Some extra details to shed light on the original post:
The three major files we use for keyboard rendering are KeyboardView.cs, KeyboardRowView.cs, and KeyView.cs.
KeyboardView (the container for the whole keyboard)
This has no trouble rendering. KeyboardView extends a LinearLayout and its OnDraw method runs, calling a Build() function to create what it needs (just a basic background which will "hold" the individual keys):
protected override void OnDraw(Canvas canvas)
{
Build();
base.OnDraw(canvas);
// background
Paint bg = new Paint(PaintFlags.AntiAlias);
bg.Color = BG; // light blue
canvas.DrawRect(0, 0, MeasuredWidth, Height, bg);
InvalidateKeys();
}
(...and Build() below...)
public void Build()
{
// only build once
if (keyLayout != null)
return;
// clear out children
RemoveAllViews();
// define sizes of stuff
if (isPortrait)
{
keyMargin = (int)(MeasuredWidth * .01f);
}
else
{
keyMargin = (int)(MeasuredHeight * .01f);
}
keyWidth = (MeasuredWidth - (keyMargin * 2)) / keyboard.MaxCols;
keyHeight = (MeasuredHeight - (keyMargin * 2)) / keyboard.Rows.Count;
// set general padding around keyboardview
SetPadding(keyMargin, keyMargin, keyMargin, keyMargin);
// build KeyLayout from the keyboard object
keyLayout = new List<List<KeyView>>();
int idx = 0;
foreach (List<Key> row in keyboard.Rows)
{
keyLayout.Add(new List<KeyView>());
// create and add new KeyboardRowView
KeyboardRowView krv = new KeyboardRowView(Context, this, idx);
AddView(krv);
// figure out if we need a margin offset for this row
int extraMargin = 0;
int numCols = CountRowCols(row);
if (numCols < keyboard.MaxCols)
{
// measure full width of the button container and the total row margin
int rowWidth = (int)(numCols * keyWidth);
int rowMargin = MeasuredWidth - (keyMargin * 2) - rowWidth;
// add the offset
extraMargin = rowMargin / 2;
}
// build keys and add them to keyLayout and KeyboardRowView
int idx2 = 0;
foreach (Key key in row)
{
int leftMargin = idx2 == 0 ? extraMargin : 0;
KeyView kv = new KeyView(Context, this, key, leftMargin);
keyLayout[idx].Add(kv);
krv.AddView(kv);
idx2++;
}
idx++;
}
}
(As a friendly reminder, we're doing this because we need a custom keyboard which can only display certain keys/commands to our users.)
KeyboardRowView (the container for each row of keys)
This also extends a LinearLayout, and also has its OnDraw method called:
protected override void OnDraw(Canvas canvas)
{
base.OnDraw(canvas);
Paint paint = new Paint();
paint.SetARGB(255, 0, 0, 0);
paint.SetStyle(Paint.Style.Stroke);
paint.StrokeWidth = 3;
canvas.DrawRGB(255, 255, 255);
canvas.DrawRect(0, 0, 100, 100, paint);
}
KeyView (the class which loads and renders each individual key)
KeyView extends View and View.IOnTouchListener. KeyView's constructor is called, but its OnDraw method is never called/executed:
// key views are always dynamically created
public KeyView(Context ctx, KeyboardView parent, Key k, int leftMargin)
: base(ctx)
{
// make sure the key will draw
SetWillNotDraw(false);
keyboard = parent;
key = k;
isDown = false;
// check for an overridden span to adjust width, if needed
int span = string.IsNullOrEmpty(key.Span) ? 1 : Convert.ToInt32(key.Span);
int keyWidth = keyboard.keyWidth + ((span - 1) * keyboard.keyWidth);
width = keyWidth;
height = keyboard.keyHeight;
// set margin
var parameters = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WrapContent,
LinearLayout.LayoutParams.MatchParent
);
parameters.LeftMargin = leftMargin;
LayoutParameters = parameters;
// set touch listener
SetOnTouchListener(this);
// enable haptic feedback for button presses
HapticFeedbackEnabled = true;
}
(...and OnDraw)
protected override void OnDraw(Canvas canvas)
{
base.OnDraw(canvas);
KeyState primary = key.Primary;
KeyState secondary = key.Secondary;
if (keyboard.swapped)
{
primary = key.Secondary != null ? key.Secondary : key.Primary;
secondary = key.Secondary != null ? key.Primary : null;
}
if (keyboard.shifted)
{
if (primary.Shift != null)
primary = primary.Shift;
if (secondary != null && secondary.Shift != null)
secondary = secondary.Shift;
}
// figure out what color the key is supposed to be
Paint bg = new Paint(PaintFlags.AntiAlias);
bg.Color = GetKeyBgColor(key.Style);
if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Lollipop)
canvas.DrawRoundRect(keyboard.keyMargin, keyboard.keyMargin, width - keyboard.keyMargin, height - keyboard.keyMargin, keyboard.keyMargin, keyboard.keyMargin, bg);
else
canvas.DrawRoundRect(new RectF(keyboard.keyMargin, keyboard.keyMargin, width - keyboard.keyMargin, height - keyboard.keyMargin), keyboard.keyMargin, keyboard.keyMargin, bg);
// draw primary key state
Paint fg = new Paint(PaintFlags.AntiAlias);
fg.TextSize = height * .5f;
fg.Color = GetKeyFgColor(key.Style);
string character = string.IsNullOrEmpty(primary.Character) ? "#" : primary.Character;
int charWidth = Convert.ToInt32(fg.MeasureText(character));
int charX = (width - charWidth) / 2;
canvas.DrawText(character, charX, (height * .7f), fg);
// draw secondary key state
if (secondary != null)
{
fg.TextSize = height * .25f;
fg.Color = GetKeyFgColor(key.Style, true);
character = string.IsNullOrEmpty(secondary.Character) ? "#" : secondary.Character;
charWidth = Convert.ToInt32(fg.MeasureText(character));
charX = width - charWidth - (keyboard.keyMargin * 2);
canvas.DrawText(character, charX, (height * .35f), fg);
}
}
I am confused. Both KeyboardView and KeyboardRowView have a SetWillNotDraw(false); function call in their constructor/initialization methods. KeyView also has the same function call, and successfully receives each key value that needs to be rendered. What I don't get is why it just...won't...draw...the...keyboard. (Argh.) When I spoke with the original poster about this, he told me that all the conditions have been met in order for the keyboard keys to be rendered. I tried attaching breakpoints to see what was preventing KeyView's OnDraw from being called, but got caught up in repeated OnMeasure function calls (and there are a lot of keys that get rendered, so that got old quick).
It's worth mentioning that we've tested it on the latest Nexus 6P smartphone (running stock Android 6.0 Marshmallow) and an old Motorola Droid 4 (with Marshmallow installed via CyanogenMod 13). When we tried it using a Xamarin Android Player emulator (running Marshmallow), it actually worked... my guess is that the emulator might be rendering the keyboard with no problem because the actual phones themselves are either
(a) restricting access somehow
(b) potentially holding on to old code and we just haven't completely removed their old .apks
(c) some other issue I haven't thought of
Thank you for your time. If anyone can think of a possible direction to go in, it would be appreciated!
Fixed by properly implementing OnMeasure and OnLayout for each custom view through the top-down approach suggested by Android documentation.
Im extending the View class in order to have a "drawable" view witch contains the desired background.
The problem is when this class works fine in other activity (same app).
Here is the class that extends View:
public class BackgroundMap extends View {
private String strmap;
private int totalWidth;
public BackgroundMap(Context context, String map, int w, int h) {
super(context);
super.setLayoutParams(new LayoutParams(w, h));
totalWidth = w;
strmap = map;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Resources res = getResources();
int eachBoxSize = totalWidth/10;
float left = 0;
float top = 0;
Paint paint = new Paint();
for(char c : strmap.toCharArray()){
Bitmap bm = Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, Terrain.getResource(c)), eachBoxSize, eachBoxSize, false);
canvas.drawBitmap(bm, left, top, paint);
if(left == totalWidth - eachBoxSize){
left = 0;
top += eachBoxSize;
}else{
left += eachBoxSize;
}
}
}
}
This view works like this: somelayout.addView(new BackgroundMap(arg..));
the String map argument means an array of terrains (char) ids that point to several little png images.
and this is how i use it in my Activity:
ly_rbtn_image = (FrameLayout) findViewById(R.id.ly_rbtn_image);
ly_rbtn_image.post(new Runnable() {
#Override
public void run() {
imagewidth = ly_rbtn_image.getWidth();
ly_rbtn_image.addView(new BackgroundMap(getApplicationContext(), str, imagewidth, imagewidth));
}
});
i check with logcat the width of parent layout and it has 225px. As you can see the image below only build the first row of bitmaps... but this IS NOT a loop problem
I check inside the custom view loop at onDraw method if there was a problem here but it iterate the whole string (100 chars, 10x10 bitmaps)
hope u can help me :S
http://deviantsart.com/1afcam8.png
solved. please delete me.
the bitmaps were pushed at right till the end of the loop. So in fact, i didnt know why this happened.
UNTIL I CHECKED THE BOUNDS OF THE RESULT VIEW IN LOGCAT x))) the view had 2000 pixels width and 20 height... there we go:
When i get the total width of the device, casually was a number divisible by 10 (10x10 grid) so in fact was lucky not having this problem before (building the same map in other view).
Then, the problem was the need to print the same map (or grid) in an other view, not big as the device width, so the total width (parent width) was not divisible by the number of columns of the grid (im ashamed x)).
I'm designing a soft keyboard and I want to change its height at run-time as the user choose between landscape and portrait mode. I know how to change key's height in xml, but I need to do it dynamically.
The only thing that came to my mind was to subclass from Keyboard and override its setKeysHeight (int height), but it seems useless as the whole keyboard stopped responding to my clicks and the height (though different from previously) didn't care about 'height' in the aforementioned function.
Any idea/workaround?
Original solution posted at https://stackoverflow.com/a/9695482/1241783 but it doesn't come with explanation so here I extend it a bit.
1) Create a new class that extends the Keyboard class that overrides the getHeight() method.
#Override
public int getHeight() {
return getKeyHeight() * 3;
}
Note: the number 3 here is your total number of rows, if your keyboard has 5 rows, put 5.
If your keyboard row height is different for each row, here you need to calculate yourself and return the total height (unit is in pixels, took me a while to figure out that it is not dp so need to convert dp to pixel for all calculations) for example:
#Override
public int getHeight() {
return row1Height + row2Height + row3Height + row4Height + row5Height;
}
2) Create a new public function in the same class.
public void changeKeyHeight(double height_modifier)
{
int height = 0;
for(Keyboard.Key key : getKeys()) {
key.height *= height_modifier;
key.y *= height_modifier;
height = key.height;
}
setKeyHeight(height);
getNearestKeys(0, 0); //somehow adding this fixed a weird bug where bottom row keys could not be pressed if keyboard height is too tall.. from the Keyboard source code seems like calling this will recalculate some values used in keypress detection calculation
}
If you're not using height_modifier but set to specific height instead, you'll need to calculate key.y position yourself.
If your keyboard row height is different for each row, you may need to check the keys, determine the row it belongs and set the height to correct value if not the keys will overlap each other. Also store the row heights in private variable to be used in getHeight() above. PS: On certain configuration I couldn't press the bottom row keys after changing keyboard height, and I found that calling getNearestKeys() fixes that though I'm not exactly sure why.
Note: key.y is y position of the key, coordinate 0 starts from the top of the keyboard, and goes down as the value increases. e.g. Coordinate 100 points to 100 pixel from the top of the keyboard :)
3) Last step is to call changeKeyHeight in your main class that extends InputMethodService. Do it inside (override it) onStartInputView() as this is where the keyboard should be redrawn after you change the height (via preference or something).
If you're looking at the Android soft keyboard sample project, it will be like this:
#Override public void onStartInputView(EditorInfo attribute, boolean restarting) {
super.onStartInputView(attribute, restarting);
// Change the key height here dynamically after getting your value from shared preference or something
mCurKeyboard.changeKeyHeight(1.5);
// Apply the selected keyboard to the input view.
mInputView.setKeyboard(mCurKeyboard);
mInputView.closing();
final InputMethodSubtype subtype = mInputMethodManager.getCurrentInputMethodSubtype();
mInputView.setSubtypeOnSpaceKey(subtype);
}
Cheers!
Extra: If you need a dp to pixel converter, here's the code:
private int convertDpToPx(int dp)
{
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
I use the following code with Android 6:
private void adjustKeyboardKeyHeight (MyKeyboard keyboard, int newKeyHeight) {
int oldKeyHeight = keyboard.getKeyHeight();
int verticalGap = keyboard.getVerticalGap();
int rows = 0;
for (Keyboard.Key key : keyboard.getKeys()) {
key.height = newKeyHeight;
int row = (key.y + verticalGap) / (oldKeyHeight + verticalGap);
key.y = row * newKeyHeight + (row - 1) * verticalGap;
rows = Math.max(rows, row + 1);
}
keyboard.setHeight(rows * newKeyHeight + (rows - 1) * verticalGap);
}
private static class MyKeyboard extends Keyboard {
private int height;
MyKeyboard (Context context, int xmlLayoutResId) {
super(context, xmlLayoutResId);
height = super.getHeight();
}
#Override public int getKeyHeight() {
return super.getKeyHeight();
}
#Override public int getVerticalGap() {
return super.getVerticalGap();
}
public void setHeight (int newHeight) {
height = newHeight;
}
#Override public int getHeight() {
return height;
}
}
Bruce's solution does not work anymore. Bottom keys stop responding after resize. Look at Keyboard.java:
public int[] getNearestKeys(int x, int y) {
if (mGridNeighbors == null) computeNearestNeighbors();
So getNearestKeys does not trigger any computations, computeNearestNeighbors() is not called more than once (and it's private, so you can't call it directly).
Instead of changing size in existing object, I modified Keyboard.Key class:
public class FTKey extends Keyboard.Key {
FTKey(Resources res, Keyboard.Row parent, int x, int y, XmlResourceParser parser) {
super(res, parent, x, y, parser);
this.height = (int) (height * FTPref.yScale);
this.y = this.height * FTPref.yScale; // This works only if all rows of the same height
}
In onStartInput() I re-create all keyboard objects anew every time:
FTPref.yScale = 0.7f;
mEnglishKeyboard = new FTKeyboard(this ...
mRussianKeyboard = new FTKeyboard(this ...
Overriding getHeight() to calculate total keyboard height is still required.
It will be tricky to have keys of different height, because you'll have to calculate not only total height, but y position of every button and it depends on the height of previous rows.
Background
Facebook app has a nice transition animation between a small image on a post, and an enlarged mode of it that the user can also zoom to it.
As I see it, the animation not only enlarges and moves the imageView according to its previous location and size, but also reveals content instead of stretching the content of the imageView.
This can be seen using the next sketch i've made:
The question
How did they do it? did they really have 2 views animating to reveal the content?
How did they make it so fluid as if it's a single view?
the only tutorial i've seen (link here) of an image that is enlarged to full screen doesn't show well when the thumbnail is set to be center-crop.
Not only that, but it works even on low API of Android.
does anybody know of a library that has a similar ability?
EDIT: I've found a way and posted an answer, but it's based on changing the layoutParams , and i think it's not efficient and recommended.
I've tried using the normal animations and other animation tricks, but for now that's the only thing that worked for me.
If anyone know what to do in order to make it work in a better way, please write it down.
Ok, i've found a possible way to do it.
i've made the layoutParams as variables that keep changing using the ObjectAnimator of the nineOldAndroids library. i think it's not the best way to achieve it since it causes a lot of onDraw and onLayout, but if the container has only a few views and doesn't change its size, maybe it's ok.
the assumption is that the imageView that i animate will take the exact needed size in the end, and that (currently) both the thumbnail and the animated imageView have the same container (but it should be easy to change it.
as i've tested, it is also possible to add zoom features by extending the TouchImageView class . you just set the scale type in the beginning to center-crop, and when the animation ends you set it back to matrix, and if you want, you can set the layoutParams to fill the entire container (and set the margin to 0,0).
i also wonder how come the AnimatorSet didn't work for me, so i will show here something that works, hoping someone could tell me what i should do.
here's the code:
MainActivity.java
public class MainActivity extends Activity {
private static final int IMAGE_RES_ID = R.drawable.test_image_res_id;
private static final int ANIM_DURATION = 5000;
private final Handler mHandler = new Handler();
private ImageView mThumbnailImageView;
private CustomImageView mFullImageView;
private Point mFitSizeBitmap;
#Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mFullImageView = (CustomImageView) findViewById(R.id.fullImageView);
mThumbnailImageView = (ImageView) findViewById(R.id.thumbnailImageView);
mHandler.postDelayed(new Runnable() {
#Override
public void run() {
prepareAndStartAnimation();
}
}, 2000);
}
private void prepareAndStartAnimation() {
final int thumbX = mThumbnailImageView.getLeft(), thumbY = mThumbnailImageView.getTop();
final int thumbWidth = mThumbnailImageView.getWidth(), thumbHeight = mThumbnailImageView.getHeight();
final View container = (View) mFullImageView.getParent();
final int containerWidth = container.getWidth(), containerHeight = container.getHeight();
final Options bitmapOptions = getBitmapOptions(getResources(), IMAGE_RES_ID);
mFitSizeBitmap = getFitSize(bitmapOptions.outWidth, bitmapOptions.outHeight, containerWidth, containerHeight);
mThumbnailImageView.setVisibility(View.GONE);
mFullImageView.setVisibility(View.VISIBLE);
mFullImageView.setContentWidth(thumbWidth);
mFullImageView.setContentHeight(thumbHeight);
mFullImageView.setContentX(thumbX);
mFullImageView.setContentY(thumbY);
runEnterAnimation(containerWidth, containerHeight);
}
private Point getFitSize(final int width, final int height, final int containerWidth, final int containerHeight) {
int resultHeight, resultWidth;
resultHeight = height * containerWidth / width;
if (resultHeight <= containerHeight) {
resultWidth = containerWidth;
} else {
resultWidth = width * containerHeight / height;
resultHeight = containerHeight;
}
return new Point(resultWidth, resultHeight);
}
public void runEnterAnimation(final int containerWidth, final int containerHeight) {
final ObjectAnimator widthAnim = ObjectAnimator.ofInt(mFullImageView, "contentWidth", mFitSizeBitmap.x)
.setDuration(ANIM_DURATION);
final ObjectAnimator heightAnim = ObjectAnimator.ofInt(mFullImageView, "contentHeight", mFitSizeBitmap.y)
.setDuration(ANIM_DURATION);
final ObjectAnimator xAnim = ObjectAnimator.ofInt(mFullImageView, "contentX",
(containerWidth - mFitSizeBitmap.x) / 2).setDuration(ANIM_DURATION);
final ObjectAnimator yAnim = ObjectAnimator.ofInt(mFullImageView, "contentY",
(containerHeight - mFitSizeBitmap.y) / 2).setDuration(ANIM_DURATION);
widthAnim.start();
heightAnim.start();
xAnim.start();
yAnim.start();
// TODO check why using AnimatorSet doesn't work here:
// final com.nineoldandroids.animation.AnimatorSet set = new AnimatorSet();
// set.playTogether(widthAnim, heightAnim, xAnim, yAnim);
}
public static BitmapFactory.Options getBitmapOptions(final Resources res, final int resId) {
final BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, bitmapOptions);
return bitmapOptions;
}
}
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<com.example.facebookstylepictureanimationtest.CustomImageView
android:id="#+id/fullImageView"
android:layout_width="0px"
android:layout_height="0px"
android:background="#33ff0000"
android:scaleType="centerCrop"
android:src="#drawable/test_image_res_id"
android:visibility="invisible" />
<ImageView
android:id="#+id/thumbnailImageView"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:scaleType="centerCrop"
android:src="#drawable/test_image_res_id" />
</RelativeLayout>
CustomImageView.java
public class CustomImageView extends ImageView {
public CustomImageView(final Context context) {
super(context);
}
public CustomImageView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public CustomImageView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
}
public void setContentHeight(final int contentHeight) {
final LayoutParams layoutParams = getLayoutParams();
layoutParams.height = contentHeight;
setLayoutParams(layoutParams);
}
public void setContentWidth(final int contentWidth) {
final LayoutParams layoutParams = getLayoutParams();
layoutParams.width = contentWidth;
setLayoutParams(layoutParams);
}
public int getContentHeight() {
return getLayoutParams().height;
}
public int getContentWidth() {
return getLayoutParams().width;
}
public int getContentX() {
return ((MarginLayoutParams) getLayoutParams()).leftMargin;
}
public void setContentX(final int contentX) {
final MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = contentX;
setLayoutParams(layoutParams);
}
public int getContentY() {
return ((MarginLayoutParams) getLayoutParams()).topMargin;
}
public void setContentY(final int contentY) {
final MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
layoutParams.topMargin = contentY;
setLayoutParams(layoutParams);
}
}
Another solution, if you just want to make an animation of an image from small to large, you can try ActivityOptions.makeThumbnailScaleUpAnimation or makeScaleUpAnimationand see if they suit you.
http://developer.android.com/reference/android/app/ActivityOptions.html#makeThumbnailScaleUpAnimation(android.view.View, android.graphics.Bitmap, int, int)
You can achieve this through Transition Apiļ¼ and this the resut gif:
essential code below:
private void zoomIn() {
ViewGroup.LayoutParams layoutParams = mImage.getLayoutParams();
int width = layoutParams.width;
int height = layoutParams.height;
layoutParams.width = (int) (width * 2);
layoutParams.height = height * 2;
mImage.setLayoutParams(layoutParams);
mImage.setScaleType(ImageView.ScaleType.FIT_CENTER);
TransitionSet transitionSet = new TransitionSet();
Transition bound = new ChangeBounds();
transitionSet.addTransition(bound);
Transition changeImageTransform = new ChangeImageTransform();
transitionSet.addTransition(changeImageTransform);
transitionSet.setDuration(1000);
TransitionManager.beginDelayedTransition(mRootView, transitionSet);
}
View demo on github
sdk version >= 21
I found a way to get a similar affect in a quick prototype. It might not be suitable for production use (I'm still investigating), but it is quick and easy.
Use a fade transition on your activity/fragment transition (which starts with the ImageView in exactly the same position). The fragment version:
final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
...etc
The activity version:
Intent intent = new Intent(context, MyDetailActivity.class);
startActivity(intent);
getActivity().overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
This gives a smooth transition without a flicker.
Adjust the layouts dynamically in the onStart() of the new fragment (you need to save member fields to the appropriate parts of your UI in onCreateView, and add some flags to ensure this code only gets called once).
#Override
public void onStart() {
super.onStart();
// Remove the padding on any layouts that the image view is inside
mMainLayout.setPadding(0, 0, 0, 0);
// Get the screen size using a utility method, e.g.
// http://stackoverflow.com/a/12082061/112705
// then work out your desired height, e.g. using the image aspect ratio.
int desiredHeight = (int) (screenWidth * imgAspectRatio);
// Resize the image to fill the whole screen width, removing
// any layout margins that it might have (you may need to remove
// padding too)
LinearLayout.LayoutParams layoutParams =
new LinearLayout.LayoutParams(screenWidth, desiredHeight);
layoutParams.setMargins(0, 0, 0, 0);
mImageView.setLayoutParams(layoutParams);
}
I think the easiest way is to animate the height of the ImageView (a regular imageview, not necessary a custom view) while keeping the scaleType to centerCrop until full height, which you can know in advance if you set the image height to wrap_content in your layout and then use a ViewTreeObserver to know when the layout has ended, so you can get the ImageView height and then set the new "collapsed" height. I have not tested it but this is how I would do it.
You can also have a look at this post, they do something similar http://nerds.airbnb.com/host-experience-android/
I'm not sure why everyone is talking about the framework. Using other peoples code can be great at times; but it sounds like what you are after is precise control over the look. By getting access to the graphics context you can have that. The task is pretty simple in any environment that has a graphics context. In android you can get it by overriding the onDraw method and using the Canvas Object. It has everything you need to draw an image at many different scales, positions and clippings. You can even use a matrix if your familiar with that type of thing.
Steps
Make sure you have exact control of positioning, scale, and clip. This means disabling any layouts or auto-alignment that might be setup inside your objects container.
Figure you out what your parameter t will be for linear interpolation and how you will want it to relate to time. How fast or slow, and will there be any easing. t should be dependent on time.
After the thumbnails are cached, load the full scale image in the background. But don't show it yet.
When the animation trigger fires, show the large image and drive your animation with your t parameter using interpolation between the initial properties' states to the final properties' states. Do this for all three properties, position, scale and clip. So for all properties do the following:
Sinterpolated = Sinitial * (t-1) + Sfinal * t;
// where
// t is between 0.0 and 1.0
// and S is the states value
// for every part of scale, position, and clip
//
// Sinitial is what you are going from
// Sfinal is what you are going to
//
// t should change from 0.0->1.0 in
// over time anywhere from 12/sec or 60/sec.
If all your properties are driven by the same parameter the animation will be smooth. As an added bonus, here is a tip for timing. As long as you can keep your t parameter between 0 and 1, easing in or out can be hacked with one line of code:
// After your t is all setup
t = t * t; // for easing in
// or
t = Math.sqrt(t); // for easing out
I made a sample code in Github.
The key of this code is using canvas.clipRect().
But, it only works when the CroppedImageview is match_parent.
To explain simply,
I leave scale and translation animation to ViewPropertyAnimator.
Then, I can focus on cropping the image.
Like above picture, calculate the clipping region, and change the clip region to final view size.
AnimationController
class ZoomAnimationController(private val view: CroppedImageView, startRect: Rect, private val viewRect: Rect, imageSize: Size) {
companion object {
const val DURATION = 300L
}
private val startViewRect: RectF
private val scale: Float
private val startClipRect: RectF
private val animatingRect: Rect
private var cropAnimation: ValueAnimator? = null
init {
val startImageRect = getProportionalRect(startRect, imageSize, ImageView.ScaleType.CENTER_CROP)
startViewRect = getProportionalRect(startImageRect, viewRect.getSize(), ImageView.ScaleType.CENTER_CROP)
scale = startViewRect.width() / viewRect.width()
val finalImageRect = getProportionalRect(viewRect, imageSize, ImageView.ScaleType.FIT_CENTER)
startClipRect = getProportionalRect(finalImageRect, startRect.getSize() / scale, ImageView.ScaleType.FIT_CENTER)
animatingRect = Rect()
startClipRect.round(animatingRect)
}
fun init() {
view.x = startViewRect.left
view.y = startViewRect.top
view.pivotX = 0f
view.pivotY = 0f
view.scaleX = scale
view.scaleY = scale
view.setClipRegion(animatingRect)
}
fun startAnimation() {
cropAnimation = createCropAnimator().apply {
start()
}
view.animate()
.x(0f)
.y(0f)
.scaleX(1f)
.scaleY(1f)
.setDuration(DURATION)
.start()
}
private fun createCropAnimator(): ValueAnimator {
return ValueAnimator.ofFloat(0f, 1f).apply {
duration = DURATION
addUpdateListener {
val weight = animatedValue as Float
animatingRect.set(
(startClipRect.left * (1 - weight) + viewRect.left * weight).toInt(),
(startClipRect.top * (1 - weight) + viewRect.top * weight).toInt(),
(startClipRect.right * (1 - weight) + viewRect.right * weight).toInt(),
(startClipRect.bottom * (1 - weight) + viewRect.bottom * weight).toInt()
)
Log.d("SSO", "animatingRect=$animatingRect")
view.setClipRegion(animatingRect)
}
}
}
private fun getProportionalRect(viewRect: Rect, imageSize: Size, scaleType: ImageView.ScaleType): RectF {
return getProportionalRect(RectF(viewRect), imageSize, scaleType)
}
private fun getProportionalRect(viewRect: RectF, imageSize: Size, scaleType: ImageView.ScaleType): RectF {
val viewRatio = viewRect.height() / viewRect.width()
if ((scaleType == ImageView.ScaleType.FIT_CENTER && viewRatio > imageSize.ratio)
|| (scaleType == ImageView.ScaleType.CENTER_CROP && viewRatio <= imageSize.ratio)) {
val width = viewRect.width()
val height = width * imageSize.ratio
val paddingY = (height - viewRect.height()) / 2f
return RectF(viewRect.left, viewRect.top - paddingY, viewRect.right, viewRect.bottom + paddingY)
} else if ((scaleType == ImageView.ScaleType.FIT_CENTER && viewRatio <= imageSize.ratio)
|| (scaleType == ImageView.ScaleType.CENTER_CROP && viewRatio > imageSize.ratio)){
val height = viewRect.height()
val width = height / imageSize.ratio
val paddingX = (width - viewRect.width()) / 2f
return RectF(viewRect.left - paddingX, viewRect.top, viewRect.right + paddingX, viewRect.bottom)
}
return RectF()
}
CroppedImageView
override fun onDraw(canvas: Canvas?) {
if (clipRect.width() > 0 && clipRect.height() > 0) {
canvas?.clipRect(clipRect)
}
super.onDraw(canvas)
}
fun setClipRegion(rect: Rect) {
clipRect.set(rect)
invalidate()
}
it only works when the CroppedImageview is match_parent,
because
The paths from start to end is included in CroppedImageView. If not, animation is not shown. So, Making it's size match_parent is easy to think.
I didn't implement the code for special case...
I am working on a project in work and there are two questions I am not sure about which I would be grateful if anyone could be tell me if this is doable or not.
A) Would it be rather difficult to have an XML file which would contain data that would be used to dictate the location of objects on screen. Was thinking storing coordinates, then parsing the coordinates, and setting up the layout in Java using the information stored in the xml document. Do you reckon this would end up being way too complicated using this method?
Now I doubt this is possible, but just want to be sure.
B) Is it possible to store EditText, TextView and other widgets etc in an XML file and parse it using Java and then add it to your layout?
Thanks in advance.
To a) and b):
We do it like this as and it works fine :)
More specifically:
A) We use a FrameLayout and dynamically add the UI elements to it. Using margins it is easy to set the size and position
B) We have an activity which can display arbitrary forms that are specified in an XML file. building it up is a few hundred lines of code but it works fine.
Some code on how to set up a UI based on XML:
First of all I assume that you know how to parse an XML file and store the relevant information in an object. The View that contains all the UI elements from the XML file is called frame in the following example.
FrameLayout frame = new FrameLayout(context);
// assemble views
// XmlElement is one element in the XML file
// page and cntnr are of type XmlElement
for (XmlElement cntnr : page.getChildren())
{
content.addView(getViewForContainer(cntnr, context));
}
You can now use frame for setContentView or add it to another ViewGroup.
Missing is the method getViewForContainer:
private static View getViewForContainer(XmlElement cntnr,
Activity context)
{
Rect bounds = cntnr.getBounds();
int w = bounds.right;
int h = bounds.bottom;
// here is your View
View ret = new View(context);
// do this for all kinds of container
// type is an enum that defines what kind of UI element
// a XML element is.
switch (cntnr.getType())
{
case IMAGE:
ret = createImage(context, pageContainer);
break;
case VIDEO:
ret = createVideo(context, pageContainer);
break;
case AUDIO:
ret = createAudio(context, pageContainer, page);
break;
...
case TEXT_FIELD:
ret = createTextField(context, pageContainer);
break;
}
return ret;
}
You do that for all kinds of UI elements. I'll just show the createImage() as example:
private static View createImage(Activity context,
XmlElement page)
{
ImageView iv = new ImageView(context);
iv.setImageBitmap(BitmapFactory.decodeFile(RessourceLoader
.getFilesDirectory() + page.getFile()));
setImageViewPosition(iv, page);
return iv;
}
Here is the method for setting the position of a View. It works only for FrameLayouts and uses margins. Also it assumes that the frame uses the whole screen. If you do not want it to fill the whole screen then use some other windowWidth and -Height.
private static void setViewPosition(View view, Activity context,
XmlElement page)
{
final int windowWidth = context.getWindowManager().getDefaultDisplay().getWidth();
final int windowHeight = context.getWindowManager().getDefaultDisplay().getHeight();
Rect bounds = page.getBounds();
int x = bounds.left;
int y = bounds.top;
int w = bounds.right;
int h = bounds.bottom;
LayoutParams params = new LayoutParams(w != 0 ? w : android.view.ViewGroup.LayoutParams.WRAP_CONTENT, h != 0 ? h : android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(x, y, w != 0 ? windowWidth - w - x : 0, h != 0 ? windowHeight - h - y : 0);
view.setLayoutParams(params);
}
As I said: A few hundred lines of code. Could also be done better using classes and factories etc ... but you get the idea on how it works :)