Header image alt text

Floppie's Blag

Entertainment, technology, and the occasional shenanigans

While developing my first couple Android apps (which didn’t make it to market), and my first one that actually did make it to market (shameless plug: Should I Wear Pants?), I noticed what is, in my opinion, a significant issue: No easy way to look at least halfway decent on all devices. We’re supporting a massive variety of different screen sizes and densities, and the best the Android SDK can provide us natively are:

What we were able to find was a PDF white paper from Vanteon Electronic Design about this very subject – auto-scaling.  They explain in further detail the two problems I outlined above, and start going into detail on a solution.  The advantages of auto-scaling include:

  • Considerably easier support for many devices.  You probably still need to make separate layouts for tablets and phones – it depends what makes sense for your app – but that’s really it.  For each form factor, you don’t have much to worry about as far as screen size/density.
  • In most cases, you only need to include one drawable – the highest resolution version – because the image will be scaled to fit.  The only exception to this is when you’re concerned about the amount of memory your activity consumes – I’m not sure how Android handles memory with scaled images, if it’ll store the full-size version in memory or just what’s displayed.

We took the solution one step further – rather than having the Activity run the scaling code, we used it and created a ScalingLinearLayout view that automatically scales itself and its contents when it’s drawn or redrawn.  We used, and tweaked slightly, the code from that PDF, in a file called Scale.java (to copy and paste this, use the “View Raw Code” button):

package com.icantbelieveitsnotcloud.lib.util;

import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

public class Scale {
	public static void scaleContents(View rootView, View container) {
		Scale.scaleContents(rootView, container, rootView.getWidth(), rootView.getHeight());
	}

	// Scales the contents of the given view so that it completely fills the given
	// container on one axis (that is, we're scaling isotropically).
	public static void scaleContents(View rootView, View container, int width, int height) {
		Log.d("notcloud.scale", "Scale::scaleContents: container: " + container.getWidth() + "x" + container.getHeight() + ".");

		// Compute the scaling ratio
		float xScale = (float)container.getWidth() / width;
		float yScale = (float)container.getHeight() / height;
		float scale = Math.min(xScale, yScale);

		// Scale our contents
		Log.d("notcloud.scale", "Scale::scaleContents: scale=" + scale + ", width=" + width + ", height=" + height + ".");
		scaleViewAndChildren(rootView, scale, 0);
	}

	// Scale the given view, its contents, and all of its children by the given factor.
	public static void scaleViewAndChildren(View root, float scale, int canary) {
		// Retrieve the view's layout information
		ViewGroup.LayoutParams layoutParams = root.getLayoutParams();

		// Scale the View itself
		if(layoutParams.width != ViewGroup.LayoutParams.MATCH_PARENT && layoutParams.width != ViewGroup.LayoutParams.WRAP_CONTENT) {
			layoutParams.width *= scale;
		}
		if(layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT && layoutParams.height != ViewGroup.LayoutParams.WRAP_CONTENT) {
			layoutParams.height *= scale;
		}

		// If the View has margins, scale those too
		if(layoutParams instanceof ViewGroup.MarginLayoutParams) {
			ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams)layoutParams;
			marginParams.leftMargin *= scale;
			marginParams.topMargin *= scale;
			marginParams.rightMargin *= scale;
			marginParams.bottomMargin *= scale;
		}
		root.setLayoutParams(layoutParams);

		// Same treatment for padding
		root.setPadding(
			(int)(root.getPaddingLeft() * scale),
			(int)(root.getPaddingTop() * scale),
			(int)(root.getPaddingRight() * scale),
			(int)(root.getPaddingBottom() * scale)
		);

		// If it's a TextView, scale the font size
		/*
		if(root instanceof TextView) {
			TextView tv = (TextView)root;
			tv.setTextSize(tv.getTextSize() * scale); //< We do NOT want to do this.
		}
		*/

		// If it's a ViewGroup, recurse!
		if(root instanceof ViewGroup) {
			ViewGroup vg = (ViewGroup)root;
			for(int i = 0; i < vg.getChildCount(); i++) {
				scaleViewAndChildren(vg.getChildAt(i), scale, canary + 1);
			}
		}
	}
}

You’ll notice that we have the TextView font scaling parts shut off.  That’s code that came in from Vanteon’s scaling, but in the apps we’ve used it in, we’ve found it undesirable.  I left it in the article because you may want to use it – if so, find the comment that says “If it’s a TextView, scale the font size” and uncomment the if() {} block that follows.

Using that Scale class, we built ScalingLinearLayout, which extends LinearLayout, as follows:

package com.icantbelieveitsnotcloud.lib.view;

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import com.icantbelieveitsnotcloud.lib.util.Scale;

public class ScalingLinearLayout extends LinearLayout {
	int baseWidth;
	int baseHeight;
	boolean alreadyScaled;
	float scale;
	int expectedWidth;
	int expectedHeight;

	public ScalingLinearLayout(Context context) {
		super(context);

		Log.d("notcloud.view", "ScalingLinearLayout: width=" + this.getWidth() + ", height=" + this.getHeight());
		this.alreadyScaled = false;
	}

	public ScalingLinearLayout(Context context, AttributeSet attributes) {
		super(context, attributes);

		Log.d("notcloud.view", "ScalingLinearLayout: width=" + this.getWidth() + ", height=" + this.getHeight());
		this.alreadyScaled = false;
	}

	public void onFinishInflate() {
		Log.d("notcloud.view", "ScalingLinearLayout::onFinishInflate: 1 width=" + this.getWidth() + ", height=" + this.getHeight());

		// Do an initial measurement of this layout with no major restrictions on size.
		// This will allow us to figure out what the original desired width and height are.
		this.measure(1000, 1000); // Adjust this up if necessary.
		this.baseWidth = this.getMeasuredWidth();
		this.baseHeight = this.getMeasuredHeight();
		Log.d("notcloud.view", "ScalingLinearLayout::onFinishInflate: 2 width=" + this.getWidth() + ", height=" + this.getHeight());

		Log.d("notcloud.view", "ScalingLinearLayout::onFinishInflate: alreadyScaled=" + this.alreadyScaled);
		Log.d("notcloud.view", "ScalingLinearLayout::onFinishInflate: scale=" + this.scale);
		if(this.alreadyScaled) {
			Scale.scaleViewAndChildren((LinearLayout)this, this.scale, 0);
		}
	}

	public void draw(Canvas canvas) {
		// Get the current width and height.
		int width = this.getWidth();
		int height = this.getHeight();

		// Figure out if we need to scale the layout.
		// We may need to scale if:
		//    1. We haven't scaled it before.
		//    2. The width has changed.
		//    3. The height has changed.
		if(!this.alreadyScaled || width != this.expectedWidth || height != this.expectedHeight) {
			// Figure out the x-scaling.
			float xScale = (float)width / this.baseWidth;
			if(this.alreadyScaled && width != this.expectedWidth) {
				xScale = (float)width / this.expectedWidth;
			}
			// Figure out the y-scaling.
			float yScale = (float)height / this.baseHeight;
			if(this.alreadyScaled && height != this.expectedHeight) {
				yScale = (float)height / this.expectedHeight;
			}

			// Scale the layout.
			this.scale = Math.min(xScale, yScale);
			Log.d("notcloud.view", "ScalingLinearLayout::onLayout: Scaling!");
			Scale.scaleViewAndChildren((LinearLayout)this, this.scale, 0);

			// Mark that we've already scaled this layout, and what
			// the width and height were when we did so.
			this.alreadyScaled = true;
			this.expectedWidth = width;
			this.expectedHeight = height;

			// Finally, return.
			return;
		}

		super.draw(canvas);
	}
}

When calling this in a layout, you’ll want to do something along these lines:

<com.icantbelieveitsnotcloud.lib.view.ScalingLinearLayout
	android:id="@+id/sllInventory"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:background="@drawable/dark_rock_tile"
	android:gravity="center">
		<RelativeLayout android:id="@+id/rlyInventory"
			android:layout_width="120px"
			android:layout_height="165px">
		</RelativeLayout>
</com.icantbelieveitsnotcloud.lib.view.ScalingLinearLayout>

And that’s it. The resulting effect is that it will scale to fill either the width or the height, whichever ends up being a smaller scale – this way it doesn’t overflow the other dimension. It operates by exploiting the fact that, before being drawn, the ScalingLinearLayout‘s size will only be enough to contain the stuff inside it. During the draw() function, though, the match_parent width and height directives will cause the full size to be returned by getWidth() and getHeight(). It then performs some simple math on the new and original dimensions, and calls the scaleViewAndChildren() method from our Scale class above.

There’s no need for any additional XML files for styleable attributes, because ScalingLinearLayout accepts exactly the same attributes as LinearLayout. You could easily adapt this to make (for example) a ScalingRelativeLayout that extends RelativeLayout, if you had such a need. If we do this for anything in the future, I’ll be sure to post it on here – but given that we’re just wrapping the ScalingLinearLayout around a RelativeLayout in this case, I don’t anticipate such a need.

Since this is published on the Internet, feel free to use it in your work – no copyright. Any questions, feel free to leave them in a comment. I’m happy to answer them.

This is so accurate it’s scary. The whole time I was reading it all I could keep thinking was “wow, yeah, that sounds like me”.

read more | digg story

Fail PHP Programmers

Posted by Floppie on 2008-05-14
Posted in ProgrammingRants  | Tagged With: , , | No Comments yet, please leave one

Disclaimer: I hate with a passion, inefficient code. I hate with a passion, unreadable code. These two factors cause this post to be strongly verbose. If you don’t like it, don’t read it.

To start this off, I want to say that PHP is one of the easiest programming languages out there that actually has a purpose. Visual Basic and C# are also both very easy, but unfortunately, they use the .SUCK framework, thus rapidly killing their usefulness. Any programmer who uses them for anything other than the simplest of tasks should be dragged into the street and beaten.

There are a number of very simple steps to follow when programming in PHP, and unfortunately, the majority of PHP sites whose code I see fail horribly to follow these. I’m not sure if the programmers were absent on “don’t suck at life” day in PHP class, or if they just don’t understand the concepts of “a processor has to parse and execute this”, “a network connection has to carry what this outputs”, and “other programmers will probably have to read this in the future”.

First and foremost, your code should be efficient. Most people will tell you that the golden rule is readability; well, that’s only true to an extent. Your code should be tabbed. However, your tabs should be tabs and not a bunch of spaces – four spaces takes up 4x the space that one tab takes. Double quotes (“) should NEVER EVER EVER be used. I have linked a benchmark of the various output methods in PHP (scroll down). I didn’t conduct this benchmark, so credit goes to “phpnet@i3x171um.com”.

There is no reason to use large APIs for things such as database abstraction (ADODB on PHP for instance). Code does not need to be portable when writing a custom app in PHP; it’s more than likely only ever going to be run on one server, and even if it’s run on other servers, the new one will more than likely be configured to act like the old one anyway. A good server administrator will ensure this. I digress; code does not need to be portable in PHP, and code like my example has to be parsed at every page request. It doesn’t even make your code much more portable, you’re still submitting raw queries; if you’re using MySQL, you’re still giving it MySQL code to execute, which isn’t going to work on every database engine anyway. And it doesn’t make the programmer’s life any easier, because the programmer still has to do error handling for every query, and ADODB is nothing but an added pain in the ass for the server and future programmers.

This is for both readability and efficiency. Your code should be tabbed, and your tabs should be actual tab characters, not a series of spaces. If you can’t tell what yours are, turn on your whitespace characters. If you can’t, get a different editor, AEdiX is great. If you use Linux and don’t feel like installing and configuring WINE to run AEdiX (a Windoze editor), BlueFish is also great. If your tabs are spaces, people who view whitespace have to sit there and look at a bunch of dots all over the screen, and the server has to look at (assuming a tab width of 4) 4 whitespace characters for every one it should have to look at. Oh yeah, and if your editor has “smart tabs”, do us all a favor and don’t use them. Every editor is going to deal with them differently, and only your editor is going to display them correctly. One minor thing that falls into tabs and whitespace, is do not leave any trailing whitespace at the end of your lines of code. It makes you look like a retard, and it makes the server work harder.

The server should never have to do constant math. For instance, if you need the number of seconds in a day, you should not be telling the server to use “24 * 60 * 60″. You should be telling the server to use 86400. If you don’t know the solution to your equation offhand (although as a programmer, especially a web programmer, you damn well should know that there are 86400 seconds in a day) and are incapable of a simple logic thread of basic math in your head, for the love of fuck, get a calculator. If you give the server “24 * 60 * 60″, it has to parse more than just the single number of 86400, it instead has to parse three numbers and two operators. It then has to execute these math operations, and if your server is running an x86-based platform, that’s another 2 CPU cycles per operation. Not to mention the added memory wasted by storing multiple values rather than just one. Oh yeah, plus the overhead of storage space on the server’s hard drives. Yes, servers have limited space, just like you and everyone else (moreso, in fact, because the 15000RPM+ hard drives that a server should be running are far smaller than the 5400RPM, 7200RPM, and maybe 10000RPM drive(s) that you have). For the same reasons (parsing overhead and storage requirements), the print operator should never be used; use echo instead.

Regarding MySQL queries. I don’t know if a lot of guys get wood for putting newlines in strings or what, but there is aboslutely no fucking reason that a MySQL query should look like this:

select address_id, addr1, addr2, city, state, zip1, zip2, assign_date, result_due_date, field_agent, agent_date,
contact_name, contact_phone, contact_email, contact_notes
from address
where status = 1

Absolutely no reason. I don’t know if it’s because PHP is the first language they’ve used that allows them to put newlines in strings and they’re exploring their new unbounded fucking freedom or what, but it’s entirely pointless, and places needless extra characters (newlines and tabs) inside the string, and the MySQL server has to parse these. It wastes memory by placing them in the string, wastes bandwidth between the PHP box and the MySQL box by having to send the extra characters, and then wastes memory on the MySQL box.

Additionally, still using the query from above, all MySQL keywords (in the case of the above example, “select”, “from”, and “where”) should be capitalized (SELECT, FROM, and WHERE). All identifiers (table names, database names, and row names) should be wrapped in backticks (ex. `address_id`, `address`, `status`), and the WHERE clause of the query should be wrapped in parentheses. Here is what that fucking monstrosity that some man calls a query should look like:

SELECT `address_id`, `addr1`, `addr2`, `city`, `state`, `zip1`, `zip2`, `assign_date`, `result_due_date`, `field_agent`, `agent_date`, `contact_name`, `contact_phone`, `contact_email`, `contact_notes` FROM `address` WHERE (`status` = 1)

(Your browser probably caused made that auto-wrap at the first and maybe second line, and will probably make the second one wrap twice, maybe three or four times…take my word for it that there are no newlines in that one.)

That’s all for now.