<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Gorges Blog &#187; Mobile Development</title>
	<atom:link href="http://blog.GORGES.us/category/mobile-development/feed/" rel="self" type="application/rss+xml" />
	<link>http://blog.GORGES.us</link>
	<description>Web Sites that Grow Your Business - our blog</description>
	<lastBuildDate>Mon, 06 Feb 2012 14:23:43 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Transition operating systems</title>
		<link>http://blog.GORGES.us/2011/05/transition-operating-systems/</link>
		<comments>http://blog.GORGES.us/2011/05/transition-operating-systems/#comments</comments>
		<pubDate>Mon, 16 May 2011 12:24:15 +0000</pubDate>
		<dc:creator>Rasmus Schultz</dc:creator>
				<category><![CDATA[Mobile Development]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[User Interface UX]]></category>

		<guid isPermaLink="false">http://blog.GORGES.us/?p=501</guid>
		<description><![CDATA[Why modern mobile operating systems are "transition" operating systems.]]></description>
			<content:encoded><![CDATA[<p>Modern mobile operating systems are all the hype, and we&#8217;re feeling it here at GORGES &#8211; clients are becoming increasingly aware of the importance of mobile technology.</p>
<p>The major contenders when it comes to mobile operating systems are <a href="http://www.android.com/" target="_blank">Android</a>, <a href="http://en.wikipedia.org/wiki/IOS_(Apple)" target="_blank">iOS</a> (more commonly known as iPhone, iPad and iPod), and <a href="http://www.microsoft.com/windowsphone/en-us/default.aspx" target="_blank">Windows Phone 7</a> &#8211; all fantastic mobile operating systems, it is hard to even begin to compare them to their predecessors.</p>
<p>But why were we suddenly (these past few years) imbued with these brilliant new OS&#8217;es?</p>
<p>The answer of course is advances in hardware &#8211; lower cost, more computation power, high resolution displays and multi-touch, more storage, faster wireless networks, longer battery life&#8230; and on top of all that, advances in nanotechnology has made everything smaller and lighter, too.</p>
<p>My point with this little article, is to share with you an observation that has surfaced in a few of my conversations lately: that these new mobile operating systems are part of a transition towards &#8220;real&#8221; operating systems running on mobile devices. In fact, I&#8217;ve taken to referring to them as &#8220;transition operating systems&#8221; myself.</p>
<p>The evolution of mobile hardware does not cease. What made these new OS&#8217;es possible, is the fact that mobile devices are now almost as fast as &#8220;real&#8221; computers &#8211; and that same progress will put an end to them, too.</p>
<p>Soon, mobile devices will be fast enough to run &#8220;real&#8221; operating systems &#8211; and when that time comes, why would you want a dedicated mobile OS, or even a mobile device dedicated to wireless communications, such as your phone? If you could run a real Windows, OSX or Linux OS on your device, why wouldn&#8217;t you? At least two of the major players in mobile hardware, <a href="http://blogs.forbes.com/briancaulfield/2011/01/24/ard-core-nvidia-could-reveal-quad-core-mobile-processor-this-year/" target="_blank">NVidia</a> and <a href="http://gadgetsteria.com/2011/02/14/qualcomm-announces-2-5ghz-quad-core-snapdragon-processor-mwc/" target="_blank">QualComm</a>, are currently racing to bring <a href="http://en.wikipedia.org/wiki/Multi-core_processor" target="_blank">quad-core processors</a> to market this year, so it may be more imminent than you think.</p>
<p>I suspect certain companies, such as Apple, are already having the same realization &#8211; it was recently <a href="http://www.9to5mac.com/31203/mac-os-x-10-7-to-borrow-some-ios-ui-features-claim" target="_blank">rumored</a> that certain features are being migrated from iOS to OSX. My guess is, they&#8217;re getting ready to add support for iOS apps to OSX, with the intent of running OSX on a mobile device in the no-too-distant future.</p>
<p>This is all speculation, of course &#8211; and either way, it doesn&#8217;t diminish the value of the mobile &#8220;transition&#8221; operating systems, which paved the way for new advances in user interface / user experience development, and have set new standards for human-machine interfaces overall.</p>
<p>And it certainly doesn&#8217;t mean that you should hold out on your business ideas &#8211; waiting for real operating systems to hit your cell phones &#8211; the new mobile platforms are already embedded in our lives, and they are inevitably here to stay, in some form or another.</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.GORGES.us/2011/05/transition-operating-systems/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Getting the CRUD out</title>
		<link>http://blog.GORGES.us/2011/03/getting-the-crud-out/</link>
		<comments>http://blog.GORGES.us/2011/03/getting-the-crud-out/#comments</comments>
		<pubDate>Fri, 18 Mar 2011 15:43:25 +0000</pubDate>
		<dc:creator>Ted Caldwell</dc:creator>
				<category><![CDATA[Mobile Development]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Web Development]]></category>
		<category><![CDATA[crud]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://blog.GORGES.us/?p=541</guid>
		<description><![CDATA[GORGES developers are constantly looking for better ways to deliver quality web applications, while not getting caught up in the CRUD]]></description>
			<content:encoded><![CDATA[<p>The topic of one of GORGES recent internal Friday Tech Talks was &#8220;Yii CRUD: Tips and Tricks.&#8221;  I proposed this because I was curious to hear war stories from other developers about the problems and solutions they ran into when building &#8220;CRUD&#8221; &#8211; principally in the Yii framework, which has become one of our favorite tools at GORGES for creating complex web applications.</p>
<p>CRUD stands for <strong>C</strong>reate, <strong>R</strong>ead, <strong>U</strong>pdate and <strong>D</strong>elete (or, for some folks, Create, Retrieve, Update and Destroy). It refers to the basic operations that any data-centered application needs to make it run &#8211; creating new objects or records, viewing them, editing them, and getting rid of them. The code that performs these operations tends to get pretty repetitive after a while, so for a seasoned developer it can become the part of the application that you most want to &#8220;get out of the way&#8221; so you can focus on the more interesting bits, like complex business logic and nifty Javascript widgets. It is also often predominant in the administrative side of an application, where there is typically less budget for fancy design and interactivity than on the public side &#8211; but it still needs to work effectively. Thus, when developers refer to it as &#8220;crud&#8221;, they don&#8217;t always have the acronym in mind.</p>
<p>Here are some of the comments I heard from our team on how to create CRUD efficiently, in order to deliver quality, maintainable, cost-effective applications to our clients:</p>
<p>1. <strong>Don&#8217;t build it at all, if you can help it </strong>- For simpler web applications, or ones that are more content-oriented, a &#8220;full stack&#8221; framework like <a href="http://www.yiiframework.com/" target="_blank">Yii</a> or <a href="http://rubyonrails.org/" target="_blank">Ruby on Rails</a> can be overkill &#8211; a platform such as <a href="http://drupal.org/" target="_blank">Drupal</a> can take care of most of the housekeeping operations, with coding (or configuration) only required for the more &#8220;custom&#8221; features of the application.</p>
<p>2. <strong>Use code generators</strong> &#8211; Yii and most other advanced web frameworks include tools for generating &#8220;skeleton&#8221; code for CRUD operations. Most developers felt that these are a good starting point, especially when first learning a framework, but after a while it becomes more efficient to write from scratch, with judicious use of copy and paste from existing similar code (but beware the <a href="http://en.wikipedia.org/wiki/Rule_of_three_%28programming%29" target="_blank">Rule of Three</a>).</p>
<p>3. <strong>Modularize your MVC components</strong> &#8211; Even though the <a href="http://en.wikipedia.org/wiki/Model%E2%80%93View%E2%80%93Controller" target="_blank">Model-View-Controller</a> idiom forces the developer to think in modular terms to some extent, it is still possible to write overly repetitive code within that idiom, and violate the <a href="http://en.wikipedia.org/wiki/Don%27t_repeat_yourself" target="_blank">DRY</a> principle. Frameworks such as Yii provide a great foundation for building a web app, but the larger and more complex an application is, the more it makes sense to build on that foundation by using inheritance, polymorphism, and other object-oriented techniques in the CRUD (as well as in the non-CRUD components).</p>
<p>4. <strong>Build widgets where appropriate</strong> &#8211; This is just a specific example of modularization; Yii provides a widget API for building components that can be reused in different areas of an application. There are many standard widgets available, for components such as date pickers, but again, in some cases custom-building just the widget you need for a given application makes sense to keep everything &#8220;DRY&#8221;.</p>
<p>5. <strong>Use advanced languages </strong>- Just like application frameworks, many of the standard web development languages provide a great basis, but leave plenty of room for additional optimization. Two tools that our team is very excited about are <a href="http://en.wikipedia.org/wiki/Haml" target="_blank">HAML</a> and <a href="http://en.wikipedia.org/wiki/Sass_%28stylesheet_language%29" target="_blank">SASS</a>, which provide more efficient and elegant syntax for generating XHTML and CSS, respectively, than coding directly to each language. I won&#8217;t go into technical details here, but both tools allow the developer to create more robust solutions with fewer keystrokes.</p>
<p>These techniques and the fact that GORGES has structured ways of  building staff efficiency, illustrate how GORGES developers are  constantly looking for better ways to deliver quality web applications &#8211;  avoiding CRUD!</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.GORGES.us/2011/03/getting-the-crud-out/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Intern Creates Smart Phone App</title>
		<link>http://blog.GORGES.us/2010/12/intern-creates-smart-phone-app/</link>
		<comments>http://blog.GORGES.us/2010/12/intern-creates-smart-phone-app/#comments</comments>
		<pubDate>Thu, 09 Dec 2010 12:56:02 +0000</pubDate>
		<dc:creator>Matt Clark</dc:creator>
				<category><![CDATA[Mobile Development]]></category>
		<category><![CDATA[Social Media]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[mobile]]></category>
		<category><![CDATA[smart phone]]></category>

		<guid isPermaLink="false">http://blog.GORGES.us/?p=438</guid>
		<description><![CDATA[New Roots student intern Patrick Putnam joins GORGES for his Intensives Week program.  In one week he created prototypes for both iPhone and Android phones.]]></description>
			<content:encoded><![CDATA[<div id="attachment_444" class="wp-caption alignright" style="width: 310px"><a href="http://blog.GORGES.us/wp-content/uploads/2010/12/Patrick-Putnam.jpg"><img class="size-medium wp-image-444 " title="Patrick Putnam" src="http://blog.GORGES.us/wp-content/uploads/2010/12/Patrick-Putnam-300x204.jpg" alt="Patrick Putnam presents his project." width="300" height="204" /></a><p class="wp-caption-text">Patrick presents his iPhone and Android apps for the New Roots Charter School to the GORGES staff.</p></div>
<p>We recently hosted a student intern at the GORGES offices.  Patrick Putnam is a student at the New Roots Charter School, and joined us for his Intensives Week program, supervised by his teacher Sarah Rubenstein-Gillis.</p>
<p>We recommended that he try to create a mobile phone app that could be used by students and staff at New Roots.  Patrick came up with several features that would be useful to have on a mobile app, for example the school academic calendar, map directions, website links, and accessing an online system so students can check assignments.</p>
<p>After an intensive week (now we know why they call it that!), Patrick created working prototypes that run on both iPhone and Android devices.  He based his solution on the GORGES mobile framework we have developed that is in turn built on top of a leading mobile development platform.  The prototype has the calendar, map, and links features, and features Patrick&#8217;s photography in the splash image.</p>
<p>Every few weeks we host &#8220;GORGES University&#8221; where one of us shares some new knowledge or tidbits during a brown bag lunch meeting in our office lounge area.  This week Patrick presented his project, followed by an equally-lively discussion about mobile technology.</p>
<p>We have enjoyed having Pat with us for his program, and GORGES will support his iPhone app store submission when the app is finalized.</p>
<p>Our hats are off to Patrick for his successful internship!</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.GORGES.us/2010/12/intern-creates-smart-phone-app/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Web Development &#8211; Adapt or Die</title>
		<link>http://blog.GORGES.us/2010/11/web-development-adapt-or-die/</link>
		<comments>http://blog.GORGES.us/2010/11/web-development-adapt-or-die/#comments</comments>
		<pubDate>Mon, 22 Nov 2010 16:41:49 +0000</pubDate>
		<dc:creator>Matt Clark</dc:creator>
				<category><![CDATA[General]]></category>
		<category><![CDATA[Mobile Development]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Web Development]]></category>
		<category><![CDATA[browser wars]]></category>
		<category><![CDATA[deployment]]></category>

		<guid isPermaLink="false">http://blog.GORGES.us/?p=276</guid>
		<description><![CDATA[The web development industry changes quickly.  Read more for insight into why and how we continue to strive to maintain our competitiveness.]]></description>
			<content:encoded><![CDATA[<p>The blog title sounds extreme, but there is truth to these words in our industry.</p>
<p>I have been developing software since high school, and building software and web applications for about thirty years.  If there is one thing I can count on, it is that the software business will continue to change.</p>
<p>Ten years ago we had primitive web browsers, and web pages were usually exclusively HTML.  Microsoft and their proprietary ActiveX technology dominated the browser wars.  As a developer, there were few debugging tools and no decent server-side or Javascript frameworks.  Developing web sites took time, and the results were clunky and crude by today&#8217;s standards.</p>
<p>I am pleased at how efficient we are nowadays and how much value we offer, since we develop great solutions at a fraction of the time and cost compared to the days of web infancy.  We have learned to leverage existing open source or proprietary packages as much as possible, and have a wealth of development, debugging, and deployment tools in our arsenal.  If we are allowed to target &#8220;modern&#8221; browsers such as IE7/IE8, Firefox and Safari, then we can count on browser support for features required by web 2.0 graphics and behaviors.</p>
<p>The web server hardware industry has also had amazing strides, and every year we see better value and better prices.  For example we recommended a single modern server for hosting a client&#8217;s complex web application instead of their previous vendor&#8217;s recommended 3-server cluster approach; the resultant performance has been similar to our estimated model, and every month for the last three years our customer has saved thousands of dollars since hosted cluster solutions are expensive.</p>
<p>Back to the blog title:  in our business, if we do not continue to learn from and embrace technology improvements, then we will lose our competitive edge.  The obvious result would be that we will no longer be quality or price competitive in the web development market.  Few other industries have this sort of pressure &#8211; consumer electronics and mobile phones are probably other examples.</p>
<p>It is interesting to note that only the software industry allows small firms to compete with larger ones, since creating new hardware products require so much more capital than software development.  That is one reason why there is so much more innovation and creativity in the software industry, which really has exploded now that laptops and app phones are ubiquitous.</p>
<p>What will the web world look like in the future?  Prognostication is not one of my strengths, but I imagine we will see more web applications tailored towards mobile solutions (app phones &amp; differently-sized tablets), continual improvements in frameworks, and probably the browser battles will continue to be fought between Microsoft, Firefox, and Google.</p>
<p>And as for myself, I plan on continuing to learn and adapt.</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.GORGES.us/2010/11/web-development-adapt-or-die/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>New iPhone App: Rye YMCA</title>
		<link>http://blog.GORGES.us/2010/10/new-iphone-app-rye-ymca/</link>
		<comments>http://blog.GORGES.us/2010/10/new-iphone-app-rye-ymca/#comments</comments>
		<pubDate>Tue, 05 Oct 2010 15:45:52 +0000</pubDate>
		<dc:creator>Matt Clark</dc:creator>
				<category><![CDATA[Mobile Development]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[mobile]]></category>

		<guid isPermaLink="false">http://blog.GORGES.us/?p=338</guid>
		<description><![CDATA[Announcing another iPhone app release:  Rye YMCA]]></description>
			<content:encoded><![CDATA[<p>We recently launched an iPhone app for the Rye YMCA.  It displays events, schedules, articles, videos, and other information useful to the members and community of the YMCA in Rye, New York.</p>
<p>To make updating the app automatic, we use content on the Y&#8217;s website. The website for the Y is modern and it sends out several RSS feeds.  We pull these feeds into the app to continually update it with the latest information. There are convenient links on the app for the Rye YMCA website, map location, Facebook, YouTube, e-mail address, and a single button to dial the Y telephone number.<br />
<br />
<img title="Rye YMCA - screenshot 1" src="../wp-content/uploads/2010/09/rye-ymca-1.png" alt="" width="160" height="240" /> <img style="padding-left: 10px;" title="Rye YMCA - screenshot 2" src="../wp-content/uploads/2010/09/rye-ymca-2.png" alt="" width="160" height="240" /> <img style="padding-left: 10px;" title="Rye YMCA - screenshot 3" src="../wp-content/uploads/2010/09/rye-ymca-3.png" alt="" width="160" height="240" /></p>
<p>The client is thrilled, and is considering adding Android and Blackberry apps as well.</p>
<p>To open the Rye YMCA link in your iTunes, <a href="http://itunes.apple.com/us/app/rye-ymca/id393265796?mt=8&amp;uo=4" target="itunes_store">click this link</a>.  If you have an iPhone or iPod Touch, download it and check it out!</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.GORGES.us/2010/10/new-iphone-app-rye-ymca/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Developing Apps within Android&#8217;s 16MB Memory Limit</title>
		<link>http://blog.GORGES.us/2010/07/developing-apps-within-androids-16mb-memory-limit/</link>
		<comments>http://blog.GORGES.us/2010/07/developing-apps-within-androids-16mb-memory-limit/#comments</comments>
		<pubDate>Fri, 16 Jul 2010 13:49:19 +0000</pubDate>
		<dc:creator>Matt Clark</dc:creator>
				<category><![CDATA[Mobile Development]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Android DDMT]]></category>
		<category><![CDATA[device apps]]></category>
		<category><![CDATA[device programming]]></category>
		<category><![CDATA[Eclipse MAT]]></category>
		<category><![CDATA[memory]]></category>
		<category><![CDATA[mobile apps]]></category>

		<guid isPermaLink="false">http://blog.GORGES.us/?p=269</guid>
		<description><![CDATA[One challenge to developing for the Android platform is how to squeeze everything into 16 megabytes of heap space.  This blog post lists several solutions for memory-limited Android apps.]]></description>
			<content:encoded><![CDATA[<p>One challenge to developing for the Android platform is how to  squeeze everything into 16 megabytes of heap space.  App-phones with  16GB and 32GB are common, but that is solid state storage and not RAM.   For Android applications, the limit for each application is 16MB (24MB  on newer Droid and Nexus One phones).</p>
<p>Images, audio, and video are  memory-intensive items, and many apps have these features. There are  tools to help monitor memory use (e.g. Eclipse MAT, Android DDMS), and  these tools are good for diagnosing problems, but you still need to  understand enough to be able to fix memory leaks.</p>
<p>Here are  some ideas for reducing the memory footprint of your application:</p>
<p><strong>Reduce  image sizes</strong>:  Lower the width, height, pixel bit-depth, and  compress your images as much as you can.  Of course when an image is  uncompressed and loaded into a Bitmap object then it takes up more  memory, but you can reduce the memory footprint by lowering the image  quality (for example see: View.setDrawingCacheQuality and  View.DRAWING_CACHE_QUALITY_LOW) or even disabling the cache on views containing images.</p>
<p><strong>Lower  audio and video bitrates:</strong> I don&#8217;t know this for a fact, but I  would guess that a lower-bitrate audio stream may have a smaller memory  requirement during playback.  For example a mono-48 kbit/second audio  file would require decoded fewer samples per second than a stereo-192  kbit/second file. (Please comment to this post if you test this theory  or know the answer.)</p>
<p><strong>Destroy or reuse objects:</strong> When  you can, re-use objects and make sure that old objects are  fully-destroyed. when they are no used anymore.  Even better, never  create objects in the first place if possible.  This is especially true  for bitmap objects &#8211; be sure to call the Bitmap.recycle() method.  Remember to clear callback methods of objects before destroying them,  because otherwise an object may not be properly returned to the memory  heap during a java garbage collection operation.</p>
<p><strong>Use final  and static</strong>:  Virtual methods take up more space, and are slower,  than static methods.  Final variables and arrays are stored in code  space and not the memory heap.  Granted the difference is very small  compared with 16MB of space, but every little bit counts!</p>
<p><strong>Separate  applications for localization</strong>:  If you are developing apps for  multiple languages, consider creating separate applications instead of  including all the language-specific strings, images, audio files, and  videos in a single application.</p>
<p><strong>Rely on external storage</strong>:   If you know that there is smart-card memory available, use that to store  data instead of in memory.</p>
<p><strong>Revert to an earlier Android SDK:</strong> When we reverted our most-memory-challenged Android application from  Android 2.2 to Android 1.5, we gained two important things: first, we  are now compatible with almost all existing Android phones; and second  we reduced our memory footprint by almost a megabyte.  This latter  statement is extremely interesting, since it indicates that the Android  framework is getting bloated by all the new features added between  versions 1.5 and 2.2.</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.GORGES.us/2010/07/developing-apps-within-androids-16mb-memory-limit/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Android Two-Dimensional ScrollView</title>
		<link>http://blog.GORGES.us/2010/06/android-two-dimensional-scrollview/</link>
		<comments>http://blog.GORGES.us/2010/06/android-two-dimensional-scrollview/#comments</comments>
		<pubDate>Wed, 02 Jun 2010 17:32:57 +0000</pubDate>
		<dc:creator>Matt Clark</dc:creator>
				<category><![CDATA[Mobile Development]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[google]]></category>
		<category><![CDATA[mobile phone]]></category>
		<category><![CDATA[mobility]]></category>
		<category><![CDATA[scrollview]]></category>
		<category><![CDATA[two-dimensional]]></category>

		<guid isPermaLink="false">http://blog.GORGES.us/?p=240</guid>
		<description><![CDATA[We developed a two-dimensional scrolling view for the Android platform, which is missing among the available base classes.  This solution was derived from combining the ScrollView and HorizontalScrollView base classes.]]></description>
			<content:encoded><![CDATA[<p>Recently, while developing mobile applications for the Android platform, we were pleasantly surprised to see how much the internal display classes work like Java&#8217;s Swing components.  The online Android documentation was good, and there were plenty of available example apps to help speed us along.</p>
<p>However there is a glaring limitation:  there are base classes for horizontal scrollviews and vertical scrollviews, but not one where one can scroll in two dimensions at the same time.</p>
<p>It would be a challenge to write a two-dimensional scrollview class from scratch.  Luckily for us, the entire Android platform is open-source, so we had access to both the vertical and horizontal scrollview source code.  After a few hours of work we had created a new TwoDScrollView class.</p>
<p>First, here is a disclaimer: this class has been &#8220;munged&#8221; together and is not bullet-proof for a true generalized solution.  For example the methods that handle sub-view focusing have not been tested, and there are some nuances about how to prioritize focused sub-views in two-dimensions.  I have a fully-stripped version of this class without sub-view focusing or key events that I&#8217;d be happy to share upon request.</p>
<p>Without further ado, here is the new TwoDScrollView class:</p>
<pre class="brush: java; collapse: true; light: false; title: ; toolbar: true; notranslate">
/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Revised 5/19/2010 by GORGES
 * Now supports two-dimensional view scrolling
 * http://GORGES.us
 */

package us.gorges.my_package;

import java.util.List;

import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.FocusFinder;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.Scroller;
import android.widget.TextView;

/**
 * Layout container for a view hierarchy that can be scrolled by the user,
 * allowing it to be larger than the physical display.  A TwoDScrollView
 * is a {@link FrameLayout}, meaning you should place one child in it
 * containing the entire contents to scroll; this child may itself be a layout
 * manager with a complex hierarchy of objects.  A child that is often used
 * is a {@link LinearLayout} in a vertical orientation, presenting a vertical
 * array of top-level items that the user can scroll through.
 *
 * &lt;p&gt;The {@link TextView} class also
 * takes care of its own scrolling, so does not require a TwoDScrollView, but
 * using the two together is possible to achieve the effect of a text view
 * within a larger container.
 */
public class TwoDScrollView extends FrameLayout {
 static final int ANIMATED_SCROLL_GAP = 250;
 static final float MAX_SCROLL_FACTOR = 0.5f;

 private long mLastScroll;

 private final Rect mTempRect = new Rect();
 private Scroller mScroller;

 /**
 * Flag to indicate that we are moving focus ourselves. This is so the
 * code that watches for focus changes initiated outside this TwoDScrollView
 * knows that it does not have to do anything.
 */
 private boolean mTwoDScrollViewMovedFocus;

 /**
 * Position of the last motion event.
 */
 private float mLastMotionY;
 private float mLastMotionX;

 /**
 * True when the layout has changed but the traversal has not come through yet.
 * Ideally the view hierarchy would keep track of this for us.
 */
 private boolean mIsLayoutDirty = true;

 /**
 * The child to give focus to in the event that a child has requested focus while the
 * layout is dirty. This prevents the scroll from being wrong if the child has not been
 * laid out before requesting focus.
 */
 private View mChildToScrollTo = null;

 /**
 * True if the user is currently dragging this TwoDScrollView around. This is
 * not the same as 'is being flinged', which can be checked by
 * mScroller.isFinished() (flinging begins when the user lifts his finger).
 */
 private boolean mIsBeingDragged = false;

 /**
 * Determines speed during touch scrolling
 */
 private VelocityTracker mVelocityTracker;

 /**
 * Whether arrow scrolling is animated.
 */
 private int mTouchSlop;
 private int mMinimumVelocity;
 private int mMaximumVelocity;

 public TwoDScrollView(Context context) {
   super(context);
   initTwoDScrollView();
 }

 public TwoDScrollView(Context context, AttributeSet attrs) {
   super(context, attrs);
   initTwoDScrollView();
 }

 public TwoDScrollView(Context context, AttributeSet attrs, int defStyle) {
   super(context, attrs, defStyle);
   initTwoDScrollView();
 }

 @Override
 protected float getTopFadingEdgeStrength() {
   if (getChildCount() == 0) {
     return 0.0f;
   }
   final int length = getVerticalFadingEdgeLength();
   if (getScrollY() &lt; length) {
     return getScrollY() / (float) length;
   }
   return 1.0f;
 }

 @Override
 protected float getBottomFadingEdgeStrength() {
   if (getChildCount() == 0) {
     return 0.0f;
   }
   final int length = getVerticalFadingEdgeLength();
   final int bottomEdge = getHeight() - getPaddingBottom();
   final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
   if (span &lt; length) {
     return span / (float) length;
   }
   return 1.0f;
 }

 @Override
 protected float getLeftFadingEdgeStrength() {
   if (getChildCount() == 0) {
     return 0.0f;
   }
   final int length = getHorizontalFadingEdgeLength();
   if (getScrollX() &lt; length) {
     return getScrollX() / (float) length;
   }
   return 1.0f;
 }

 @Override
 protected float getRightFadingEdgeStrength() {
   if (getChildCount() == 0) {
     return 0.0f;
   }
   final int length = getHorizontalFadingEdgeLength();
   final int rightEdge = getWidth() - getPaddingRight();
   final int span = getChildAt(0).getRight() - getScrollX() - rightEdge;
   if (span &lt; length) {
     return span / (float) length;
   }
   return 1.0f;
 }

 /**
 * @return The maximum amount this scroll view will scroll in response to
 *   an arrow event.
 */
 public int getMaxScrollAmountVertical() {
   return (int) (MAX_SCROLL_FACTOR * getHeight());
 }
 public int getMaxScrollAmountHorizontal() {
   return (int) (MAX_SCROLL_FACTOR * getWidth());
 }

 private void initTwoDScrollView() {
   mScroller = new Scroller(getContext());
   setFocusable(true);
   setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
   setWillNotDraw(false);
   final ViewConfiguration configuration = ViewConfiguration.get(getContext());
   mTouchSlop = configuration.getScaledTouchSlop();
   mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
   mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
 }

 @Override
 public void addView(View child) {
   if (getChildCount() &gt; 0) {
     throw new IllegalStateException(&quot;TwoDScrollView can host only one direct child&quot;);
   }
   super.addView(child);
 }

 @Override
 public void addView(View child, int index) {
   if (getChildCount() &gt; 0) {
     throw new IllegalStateException(&quot;TwoDScrollView can host only one direct child&quot;);
   }
   super.addView(child, index);
 }

 @Override
 public void addView(View child, ViewGroup.LayoutParams params) {
   if (getChildCount() &gt; 0) {
     throw new IllegalStateException(&quot;TwoDScrollView can host only one direct child&quot;);
   }
   super.addView(child, params);
 }

 @Override
 public void addView(View child, int index, ViewGroup.LayoutParams params) {
   if (getChildCount() &gt; 0) {
     throw new IllegalStateException(&quot;TwoDScrollView can host only one direct child&quot;);
   }
   super.addView(child, index, params);
 }

 /**
 * @return Returns true this TwoDScrollView can be scrolled
 */
 private boolean canScroll() {
   View child = getChildAt(0);
   if (child != null) {
     int childHeight = child.getHeight();
     int childWidth = child.getWidth();
     return (getHeight() &lt; childHeight + getPaddingTop() + getPaddingBottom()) ||
            (getWidth() &lt; childWidth + getPaddingLeft() + getPaddingRight());
   }
   return false;
 }

 @Override
 public boolean dispatchKeyEvent(KeyEvent event) {
   // Let the focused view and/or our descendants get the key first
   boolean handled = super.dispatchKeyEvent(event);
   if (handled) {
     return true;
   }
   return executeKeyEvent(event);
 }

 /**
 * You can call this function yourself to have the scroll view perform
 * scrolling from a key event, just as if the event had been dispatched to
 * it by the view hierarchy.
 *
 * @param event The key event to execute.
 * @return Return true if the event was handled, else false.
 */
 public boolean executeKeyEvent(KeyEvent event) {
   mTempRect.setEmpty();
   if (!canScroll()) {
     if (isFocused()) {
       View currentFocused = findFocus();
       if (currentFocused == this) currentFocused = null;
       View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN);
       return nextFocused != null &amp;&amp; nextFocused != this &amp;&amp; nextFocused.requestFocus(View.FOCUS_DOWN);
     }
     return false;
   }
   boolean handled = false;
   if (event.getAction() == KeyEvent.ACTION_DOWN) {
     switch (event.getKeyCode()) {
       case KeyEvent.KEYCODE_DPAD_UP:
         if (!event.isAltPressed()) {
           handled = arrowScroll(View.FOCUS_UP, false);
         } else {
           handled = fullScroll(View.FOCUS_UP, false);
         }
         break;
       case KeyEvent.KEYCODE_DPAD_DOWN:
         if (!event.isAltPressed()) {
           handled = arrowScroll(View.FOCUS_DOWN, false);
         } else {
           handled = fullScroll(View.FOCUS_DOWN, false);
         }
         break;
       case KeyEvent.KEYCODE_DPAD_LEFT:
         if (!event.isAltPressed()) {
           handled = arrowScroll(View.FOCUS_LEFT, true);
         } else {
           handled = fullScroll(View.FOCUS_LEFT, true);
         }
         break;
       case KeyEvent.KEYCODE_DPAD_RIGHT:
         if (!event.isAltPressed()) {
           handled = arrowScroll(View.FOCUS_RIGHT, true);
         } else {
           handled = fullScroll(View.FOCUS_RIGHT, true);
         }
         break;
     }
   }
   return handled;
 }

 @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
   /*
   * This method JUST determines whether we want to intercept the motion.
   * If we return true, onMotionEvent will be called and we do the actual
   * scrolling there.
   *
   * Shortcut the most recurring case: the user is in the dragging
   * state and he is moving his finger.  We want to intercept this
   * motion.
   */
   final int action = ev.getAction();
   if ((action == MotionEvent.ACTION_MOVE) &amp;&amp; (mIsBeingDragged)) {
     return true;
   }
   if (!canScroll()) {
     mIsBeingDragged = false;
     return false;
   }
   final float y = ev.getY();
   final float x = ev.getX();
   switch (action) {
     case MotionEvent.ACTION_MOVE:
       /*
       * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
       * whether the user has moved far enough from his original down touch.
       */
       /*
       * Locally do absolute value. mLastMotionY is set to the y value
       * of the down event.
       */
       final int yDiff = (int) Math.abs(y - mLastMotionY);
       final int xDiff = (int) Math.abs(x - mLastMotionX);
       if (yDiff &gt; mTouchSlop || xDiff &gt; mTouchSlop) {
         mIsBeingDragged = true;
       }
       break;

     case MotionEvent.ACTION_DOWN:
       /* Remember location of down touch */
       mLastMotionY = y;
       mLastMotionX = x;

       /*
       * If being flinged and user touches the screen, initiate drag;
       * otherwise don't.  mScroller.isFinished should be false when
       * being flinged.
       */
       mIsBeingDragged = !mScroller.isFinished();
       break;

     case MotionEvent.ACTION_CANCEL:
     case MotionEvent.ACTION_UP:
       /* Release the drag */
       mIsBeingDragged = false;
       break;
   }

   /*
   * The only time we want to intercept motion events is if we are in the
   * drag mode.
   */
   return mIsBeingDragged;
 }

 @Override
 public boolean onTouchEvent(MotionEvent ev) {

   if (ev.getAction() == MotionEvent.ACTION_DOWN &amp;&amp; ev.getEdgeFlags() != 0) {
     // Don't handle edge touches immediately -- they may actually belong to one of our
     // descendants.
     return false;
   }

   if (!canScroll()) {
     return false;
   }

   if (mVelocityTracker == null) {
     mVelocityTracker = VelocityTracker.obtain();
   }
   mVelocityTracker.addMovement(ev);

   final int action = ev.getAction();
   final float y = ev.getY();
   final float x = ev.getX();

   switch (action) {
     case MotionEvent.ACTION_DOWN:
       /*
       * If being flinged and user touches, stop the fling. isFinished
       * will be false if being flinged.
       */
       if (!mScroller.isFinished()) {
         mScroller.abortAnimation();
       }

       // Remember where the motion event started
       mLastMotionY = y;
       mLastMotionX = x;
       break;
     case MotionEvent.ACTION_MOVE:
       // Scroll to follow the motion event
       int deltaX = (int) (mLastMotionX - x);
       int deltaY = (int) (mLastMotionY - y);
       mLastMotionX = x;
       mLastMotionY = y;

       if (deltaX &lt; 0) {
         if (getScrollX() &lt; 0) {
           deltaX = 0;
         }
       } else if (deltaX &gt; 0) {
         final int rightEdge = getWidth() - getPaddingRight();
         final int availableToScroll = getChildAt(0).getRight() - getScrollX() - rightEdge;
         if (availableToScroll &gt; 0) {
           deltaX = Math.min(availableToScroll, deltaX);
         } else {
           deltaX = 0;
         }
       }
       if (deltaY &lt; 0) {
         if (getScrollY() &lt; 0) {
           deltaY = 0;
         }
       } else if (deltaY &gt; 0) {
         final int bottomEdge = getHeight() - getPaddingBottom();
         final int availableToScroll = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
         if (availableToScroll &gt; 0) {
           deltaY = Math.min(availableToScroll, deltaY);
         } else {
           deltaY = 0;
         }
       }
       if (deltaY != 0 || deltaX != 0)
         scrollBy(deltaX, deltaY);
       break;
     case MotionEvent.ACTION_UP:
         final VelocityTracker velocityTracker = mVelocityTracker;
         velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
         int initialXVelocity = (int) velocityTracker.getXVelocity();
         int initialYVelocity = (int) velocityTracker.getYVelocity();
         if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) &gt; mMinimumVelocity) &amp;&amp; getChildCount() &gt; 0) {
           fling(-initialXVelocity, -initialYVelocity);
         }
         if (mVelocityTracker != null) {
           mVelocityTracker.recycle();
           mVelocityTracker = null;
         }
   }
   return true;
 }

 /**
  * Finds the next focusable component that fits in this View's bounds
  * (excluding fading edges) pretending that this View's top is located at
  * the parameter top.
  *
  * @param topFocus           look for a candidate is the one at the top of the bounds
  *                           if topFocus is true, or at the bottom of the bounds if topFocus is
  *                           false
  * @param top                the top offset of the bounds in which a focusable must be
  *                           found (the fading edge is assumed to start at this position)
  * @param preferredFocusable the View that has highest priority and will be
  *                           returned if it is within my bounds (null is valid)
  * @return the next focusable component in the bounds or null if none can be
  *         found
  */
 private View findFocusableViewInMyBounds(final boolean topFocus, final int top, final boolean leftFocus, final int left, View preferredFocusable) {
   /*
   * The fading edge's transparent side should be considered for focus
   * since it's mostly visible, so we divide the actual fading edge length
   * by 2.
   */
   final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2;
   final int topWithoutFadingEdge = top + verticalFadingEdgeLength;
   final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength;
   final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
   final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength;
   final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength;

   if ((preferredFocusable != null)
     &amp;&amp; (preferredFocusable.getTop() &lt; bottomWithoutFadingEdge)
     &amp;&amp; (preferredFocusable.getBottom() &gt; topWithoutFadingEdge)
     &amp;&amp; (preferredFocusable.getLeft() &lt; rightWithoutFadingEdge)
     &amp;&amp; (preferredFocusable.getRight() &gt; leftWithoutFadingEdge)) {
     return preferredFocusable;
   }
   return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge, leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge);
 }

 /**
 * Finds the next focusable component that fits in the specified bounds.
 * &lt;/p&gt;
 *
 * @param topFocus look for a candidate is the one at the top of the bounds
 *                 if topFocus is true, or at the bottom of the bounds if topFocus is
 *                 false
 * @param top      the top offset of the bounds in which a focusable must be
 *                 found
 * @param bottom   the bottom offset of the bounds in which a focusable must
 *                 be found
 * @return the next focusable component in the bounds or null if none can
 *         be found
 */
 private View findFocusableViewInBounds(boolean topFocus, int top, int bottom, boolean leftFocus, int left, int right) {
   List&lt;View&gt; focusables = getFocusables(View.FOCUS_FORWARD);
   View focusCandidate = null;

   /*
   * A fully contained focusable is one where its top is below the bound's
   * top, and its bottom is above the bound's bottom. A partially
   * contained focusable is one where some part of it is within the
   * bounds, but it also has some part that is not within bounds.  A fully contained
   * focusable is preferred to a partially contained focusable.
   */
   boolean foundFullyContainedFocusable = false;

   int count = focusables.size();
   for (int i = 0; i &lt; count; i++) {
     View view = focusables.get(i);
     int viewTop = view.getTop();
     int viewBottom = view.getBottom();
     int viewLeft = view.getLeft();
     int viewRight = view.getRight();

     if (top &lt; viewBottom &amp;&amp; viewTop &lt; bottom &amp;&amp; left &lt; viewRight &amp;&amp; viewLeft &lt; right) {
       /*
       * the focusable is in the target area, it is a candidate for
       * focusing
       */
       final boolean viewIsFullyContained = (top &lt; viewTop) &amp;&amp; (viewBottom &lt; bottom) &amp;&amp; (left &lt; viewLeft) &amp;&amp; (viewRight &lt; right);
       if (focusCandidate == null) {
         /* No candidate, take this one */
         focusCandidate = view;
         foundFullyContainedFocusable = viewIsFullyContained;
       } else {
         final boolean viewIsCloserToVerticalBoundary =
           (topFocus &amp;&amp; viewTop &lt; focusCandidate.getTop()) ||
           (!topFocus &amp;&amp; viewBottom &gt; focusCandidate.getBottom());
         final boolean viewIsCloserToHorizontalBoundary =
           (leftFocus &amp;&amp; viewLeft &lt; focusCandidate.getLeft()) ||
           (!leftFocus &amp;&amp; viewRight &gt; focusCandidate.getRight());
         if (foundFullyContainedFocusable) {
           if (viewIsFullyContained &amp;&amp; viewIsCloserToVerticalBoundary &amp;&amp; viewIsCloserToHorizontalBoundary) {
             /*
              * We're dealing with only fully contained views, so
              * it has to be closer to the boundary to beat our
              * candidate
              */
             focusCandidate = view;
           }
         } else {
           if (viewIsFullyContained) {
             /* Any fully contained view beats a partially contained view */
             focusCandidate = view;
             foundFullyContainedFocusable = true;
           } else if (viewIsCloserToVerticalBoundary &amp;&amp; viewIsCloserToHorizontalBoundary) {
             /*
              * Partially contained view beats another partially
              * contained view if it's closer
              */
             focusCandidate = view;
           }
         }
       }
     }
   }
   return focusCandidate;
 }

 /**
  * &lt;p&gt;Handles scrolling in response to a &quot;home/end&quot; shortcut press. This
  * method will scroll the view to the top or bottom and give the focus
  * to the topmost/bottommost component in the new visible area. If no
  * component is a good candidate for focus, this scrollview reclaims the
  * focus.&lt;/p&gt;
  *
  * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
  *                  to go the top of the view or
  *                  {@link android.view.View#FOCUS_DOWN} to go the bottom
  * @return true if the key event is consumed by this method, false otherwise
  */
 public boolean fullScroll(int direction, boolean horizontal) {
   if (!horizontal) {
     boolean down = direction == View.FOCUS_DOWN;
     int height = getHeight();
     mTempRect.top = 0;
     mTempRect.bottom = height;
     if (down) {
       int count = getChildCount();
       if (count &gt; 0) {
         View view = getChildAt(count - 1);
         mTempRect.bottom = view.getBottom();
         mTempRect.top = mTempRect.bottom - height;
       }
     }
     return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0);
   } else {
     boolean right = direction == View.FOCUS_DOWN;
     int width = getWidth();
     mTempRect.left = 0;
     mTempRect.right = width;
     if (right) {
       int count = getChildCount();
       if (count &gt; 0) {
         View view = getChildAt(count - 1);
         mTempRect.right = view.getBottom();
         mTempRect.left = mTempRect.right - width;
       }
     }
     return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom);
   }
 }

 /**
  * &lt;p&gt;Scrolls the view to make the area defined by &lt;code&gt;top&lt;/code&gt; and
  * &lt;code&gt;bottom&lt;/code&gt; visible. This method attempts to give the focus
  * to a component visible in this area. If no component can be focused in
  * the new visible area, the focus is reclaimed by this scrollview.&lt;/p&gt;
  *
  * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
  *                  to go upward
  *                  {@link android.view.View#FOCUS_DOWN} to downward
  * @param top       the top offset of the new area to be made visible
  * @param bottom    the bottom offset of the new area to be made visible
  * @return true if the key event is consumed by this method, false otherwise
  */
 private boolean scrollAndFocus(int directionY, int top, int bottom, int directionX, int left, int right) {
   boolean handled = true;
   int height = getHeight();
   int containerTop = getScrollY();
   int containerBottom = containerTop + height;
   boolean up = directionY == View.FOCUS_UP;
   int width = getWidth();
   int containerLeft = getScrollX();
   int containerRight = containerLeft + width;
   boolean leftwards = directionX == View.FOCUS_UP;
   View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right);
   if (newFocused == null) {
     newFocused = this;
   }
   if ((top &gt;= containerTop &amp;&amp; bottom &lt;= containerBottom) || (left &gt;= containerLeft &amp;&amp; right &lt;= containerRight)) {
     handled = false;
   } else {
     int deltaY = up ? (top - containerTop) : (bottom - containerBottom);
     int deltaX = leftwards ? (left - containerLeft) : (right - containerRight);
     doScroll(deltaX, deltaY);
   }
   if (newFocused != findFocus() &amp;&amp; newFocused.requestFocus(directionY)) {
     mTwoDScrollViewMovedFocus = true;
     mTwoDScrollViewMovedFocus = false;
   }
   return handled;
 }

 /**
  * Handle scrolling in response to an up or down arrow click.
  *
  * @param direction The direction corresponding to the arrow key that was
  *                  pressed
  * @return True if we consumed the event, false otherwise
  */
 public boolean arrowScroll(int direction, boolean horizontal) {
   View currentFocused = findFocus();
   if (currentFocused == this) currentFocused = null;
   View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
   final int maxJump = horizontal ? getMaxScrollAmountHorizontal() : getMaxScrollAmountVertical();

   if (!horizontal) {
     if (nextFocused != null) {
       nextFocused.getDrawingRect(mTempRect);
       offsetDescendantRectToMyCoords(nextFocused, mTempRect);
       int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
       doScroll(0, scrollDelta);
       nextFocused.requestFocus(direction);
     } else {
       // no new focus
       int scrollDelta = maxJump;
       if (direction == View.FOCUS_UP &amp;&amp; getScrollY() &lt; scrollDelta) {
         scrollDelta = getScrollY();
       } else if (direction == View.FOCUS_DOWN) {
         if (getChildCount() &gt; 0) {
           int daBottom = getChildAt(0).getBottom();
           int screenBottom = getScrollY() + getHeight();
           if (daBottom - screenBottom &lt; maxJump) {
             scrollDelta = daBottom - screenBottom;
           }
         }
       }
       if (scrollDelta == 0) {
         return false;
       }
       doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
     }
   } else {
     if (nextFocused != null) {
       nextFocused.getDrawingRect(mTempRect);
       offsetDescendantRectToMyCoords(nextFocused, mTempRect);
       int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
       doScroll(scrollDelta, 0);
       nextFocused.requestFocus(direction);
     } else {
       // no new focus
       int scrollDelta = maxJump;
       if (direction == View.FOCUS_UP &amp;&amp; getScrollY() &lt; scrollDelta) {
         scrollDelta = getScrollY();
       } else if (direction == View.FOCUS_DOWN) {
         if (getChildCount() &gt; 0) {
           int daBottom = getChildAt(0).getBottom();
           int screenBottom = getScrollY() + getHeight();
           if (daBottom - screenBottom &lt; maxJump) {
             scrollDelta = daBottom - screenBottom;
           }
         }
       }
       if (scrollDelta == 0) {
         return false;
       }
       doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0);
     }
   }
   return true;
 }

 /**
  * Smooth scroll by a Y delta
  *
  * @param delta the number of pixels to scroll by on the Y axis
  */
 private void doScroll(int deltaX, int deltaY) {
   if (deltaX != 0 || deltaY != 0) {
     smoothScrollBy(deltaX, deltaY);
   }
 }

 /**
  * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
  *
  * @param dx the number of pixels to scroll by on the X axis
  * @param dy the number of pixels to scroll by on the Y axis
  */
 public final void smoothScrollBy(int dx, int dy) {
   long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
   if (duration &gt; ANIMATED_SCROLL_GAP) {
     mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
     awakenScrollBars(mScroller.getDuration());
     invalidate();
   } else {
     if (!mScroller.isFinished()) {
       mScroller.abortAnimation();
     }
     scrollBy(dx, dy);
   }
   mLastScroll = AnimationUtils.currentAnimationTimeMillis();
 }

 /**
  * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
  *
  * @param x the position where to scroll on the X axis
  * @param y the position where to scroll on the Y axis
  */
 public final void smoothScrollTo(int x, int y) {
   smoothScrollBy(x - getScrollX(), y - getScrollY());
 }

 /**
  * &lt;p&gt;The scroll range of a scroll view is the overall height of all of its
  * children.&lt;/p&gt;
  */
 @Override
 protected int computeVerticalScrollRange() {
   int count = getChildCount();
   return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
 }
 @Override
 protected int computeHorizontalScrollRange() {
   int count = getChildCount();
   return count == 0 ? getWidth() : (getChildAt(0)).getRight();
 }

 @Override
 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
   ViewGroup.LayoutParams lp = child.getLayoutParams();
   int childWidthMeasureSpec;
   int childHeightMeasureSpec;

   childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width);
   childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

   child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
 }

 @Override
 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
   final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
   final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
   getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
   final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);

   child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
 }

 @Override
 public void computeScroll() {
   if (mScroller.computeScrollOffset()) {
     // This is called at drawing time by ViewGroup.  We don't want to
     // re-show the scrollbars at this point, which scrollTo will do,
     // so we replicate most of scrollTo here.
     //
     //         It's a little odd to call onScrollChanged from inside the drawing.
     //
     //         It is, except when you remember that computeScroll() is used to
     //         animate scrolling. So unless we want to defer the onScrollChanged()
     //         until the end of the animated scrolling, we don't really have a
     //         choice here.
     //
     //         I agree.  The alternative, which I think would be worse, is to post
     //         something and tell the subclasses later.  This is bad because there
     //         will be a window where mScrollX/Y is different from what the app
     //         thinks it is.
     //
     int oldX = getScrollX();
     int oldY = getScrollY();
     int x = mScroller.getCurrX();
     int y = mScroller.getCurrY();
     if (getChildCount() &gt; 0) {
       View child = getChildAt(0);
       scrollTo(clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()),
       clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()));
     } else {
       scrollTo(x, y);
     }
     if (oldX != getScrollX() || oldY != getScrollY()) {
       onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
     }

     // Keep on drawing until the animation has finished.
     postInvalidate();
   }
 }

 /**
  * Scrolls the view to the given child.
  *
  * @param child the View to scroll to
  */
 private void scrollToChild(View child) {
   child.getDrawingRect(mTempRect);
   /* Offset from child's local coordinates to TwoDScrollView coordinates */
   offsetDescendantRectToMyCoords(child, mTempRect);
   int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
   if (scrollDelta != 0) {
     scrollBy(0, scrollDelta);
   }
 }

 /**
  * If rect is off screen, scroll just enough to get it (or at least the
  * first screen size chunk of it) on screen.
  *
  * @param rect      The rectangle.
  * @param immediate True to scroll immediately without animation
  * @return true if scrolling was performed
  */
 private boolean scrollToChildRect(Rect rect, boolean immediate) {
   final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
   final boolean scroll = delta != 0;
   if (scroll) {
     if (immediate) {
       scrollBy(0, delta);
     } else {
       smoothScrollBy(0, delta);
     }
   }
   return scroll;
 }

 /**
  * Compute the amount to scroll in the Y direction in order to get
  * a rectangle completely on the screen (or, if taller than the screen,
  * at least the first screen size chunk of it).
  *
  * @param rect The rect.
  * @return The scroll delta.
  */
 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
   if (getChildCount() == 0) return 0;
   int height = getHeight();
   int screenTop = getScrollY();
   int screenBottom = screenTop + height;
   int fadingEdge = getVerticalFadingEdgeLength();
   // leave room for top fading edge as long as rect isn't at very top
   if (rect.top &gt; 0) {
     screenTop += fadingEdge;
   }

   // leave room for bottom fading edge as long as rect isn't at very bottom
   if (rect.bottom &lt; getChildAt(0).getHeight()) {
     screenBottom -= fadingEdge;
   }
   int scrollYDelta = 0;
   if (rect.bottom &gt; screenBottom &amp;&amp; rect.top &gt; screenTop) {
     // need to move down to get it in view: move down just enough so
     // that the entire rectangle is in view (or at least the first
     // screen size chunk).
     if (rect.height() &gt; height) {
       // just enough to get screen size chunk on
       scrollYDelta += (rect.top - screenTop);
     } else {
       // get entire rect at bottom of screen
       scrollYDelta += (rect.bottom - screenBottom);
     }

     // make sure we aren't scrolling beyond the end of our content
     int bottom = getChildAt(0).getBottom();
     int distanceToBottom = bottom - screenBottom;
     scrollYDelta = Math.min(scrollYDelta, distanceToBottom);

   } else if (rect.top &lt; screenTop &amp;&amp; rect.bottom &lt; screenBottom) {
     // need to move up to get it in view: move up just enough so that
     // entire rectangle is in view (or at least the first screen
     // size chunk of it).

     if (rect.height() &gt; height) {
       // screen size chunk
       scrollYDelta -= (screenBottom - rect.bottom);
     } else {
       // entire rect at top
       scrollYDelta -= (screenTop - rect.top);
     }

     // make sure we aren't scrolling any further than the top our content
     scrollYDelta = Math.max(scrollYDelta, -getScrollY());
   }
   return scrollYDelta;
 }

 @Override
 public void requestChildFocus(View child, View focused) {
   if (!mTwoDScrollViewMovedFocus) {
     if (!mIsLayoutDirty) {
       scrollToChild(focused);
     } else {
       // The child may not be laid out yet, we can't compute the scroll yet
       mChildToScrollTo = focused;
     }
   }
   super.requestChildFocus(child, focused);
 }

 /**
  * When looking for focus in children of a scroll view, need to be a little
  * more careful not to give focus to something that is scrolled off screen.
  *
  * This is more expensive than the default {@link android.view.ViewGroup}
  * implementation, otherwise this behavior might have been made the default.
  */
 @Override
 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
   // convert from forward / backward notation to up / down / left / right
   // (ugh).
   if (direction == View.FOCUS_FORWARD) {
     direction = View.FOCUS_DOWN;
   } else if (direction == View.FOCUS_BACKWARD) {
     direction = View.FOCUS_UP;
   }

   final View nextFocus = previouslyFocusedRect == null ?
   FocusFinder.getInstance().findNextFocus(this, null, direction) :
   FocusFinder.getInstance().findNextFocusFromRect(this,
   previouslyFocusedRect, direction);

   if (nextFocus == null) {
     return false;
   }

   return nextFocus.requestFocus(direction, previouslyFocusedRect);
 }

 @Override
 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
   // offset into coordinate space of this scroll view
   rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
   return scrollToChildRect(rectangle, immediate);
 }

 @Override
 public void requestLayout() {
   mIsLayoutDirty = true;
   super.requestLayout();
 }

 @Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {
   super.onLayout(changed, l, t, r, b);
   mIsLayoutDirty = false;
   // Give a child focus if it needs it
   if (mChildToScrollTo != null &amp;&amp; isViewDescendantOf(mChildToScrollTo, this)) {
     scrollToChild(mChildToScrollTo);
   }
   mChildToScrollTo = null;

   // Calling this with the present values causes it to re-clam them
   scrollTo(getScrollX(), getScrollY());
 }

 @Override
 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
   super.onSizeChanged(w, h, oldw, oldh);

   View currentFocused = findFocus();
   if (null == currentFocused || this == currentFocused)
     return;

   // If the currently-focused view was visible on the screen when the
   // screen was at the old height, then scroll the screen to make that
   // view visible with the new screen height.
   currentFocused.getDrawingRect(mTempRect);
   offsetDescendantRectToMyCoords(currentFocused, mTempRect);
   int scrollDeltaX = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
   int scrollDeltaY = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
   doScroll(scrollDeltaX, scrollDeltaY);
 }

 /**
  * Return true if child is an descendant of parent, (or equal to the parent).
  */
 private boolean isViewDescendantOf(View child, View parent) {
   if (child == parent) {
     return true;
   }

   final ViewParent theParent = child.getParent();
   return (theParent instanceof ViewGroup) &amp;&amp; isViewDescendantOf((View) theParent, parent);
 }

 /**
  * Fling the scroll view
  *
  * @param velocityY The initial velocity in the Y direction. Positive
  *                  numbers mean that the finger/curor is moving down the screen,
  *                  which means we want to scroll towards the top.
  */
 public void fling(int velocityX, int velocityY) {
   if (getChildCount() &gt; 0) {
     int height = getHeight() - getPaddingBottom() - getPaddingTop();
     int bottom = getChildAt(0).getHeight();
     int width = getWidth() - getPaddingRight() - getPaddingLeft();
     int right = getChildAt(0).getWidth();

     mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0, bottom - height);

     final boolean movingDown = velocityY &gt; 0;
     final boolean movingRight = velocityX &gt; 0;

     View newFocused = findFocusableViewInMyBounds(movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus());
     if (newFocused == null) {
       newFocused = this;
     }

     if (newFocused != findFocus() &amp;&amp; newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP)) {
       mTwoDScrollViewMovedFocus = true;
       mTwoDScrollViewMovedFocus = false;
     }

     awakenScrollBars(mScroller.getDuration());
     invalidate();
   }
 }

 /**
  * {@inheritDoc}
  *
  * &lt;p&gt;This version also clamps the scrolling to the bounds of our child.
  */
 public void scrollTo(int x, int y) {
   // we rely on the fact the View.scrollBy calls scrollTo.
   if (getChildCount() &gt; 0) {
     View child = getChildAt(0);
     x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
     y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
     if (x != getScrollX() || y != getScrollY()) {
       super.scrollTo(x, y);
     }
   }
 }

 private int clamp(int n, int my, int child) {
   if (my &gt;= child || n &lt; 0) {
     /* my &gt;= child is this case:
      *                    |--------------- me ---------------|
      *     |------ child ------|
      * or
      *     |--------------- me ---------------|
      *            |------ child ------|
      * or
      *     |--------------- me ---------------|
      *                                  |------ child ------|
      *
      * n &lt; 0 is this case:
      *     |------ me ------|
      *                    |-------- child --------|
      *     |-- mScrollX --|
      */
     return 0;
   }
   if ((my+n) &gt; child) {
     /* this case:
      *                    |------ me ------|
      *     |------ child ------|
      *     |-- mScrollX --|
      */
     return child-my;
   }
   return n;
 }
}
&lt;pre&gt;</pre>
<p>In hindsight I think I know why two-dimensioning scrolling is not inherently included:  it is a memory hog.  There is an emphasis for beautiful graphics in the framework to achieve the best user experience; examples of this emphasis are true-color (24-bit) pixels, alpha transparency channel support, smooth scrolling, and display caching.  However the Android system restricts an application to 16MB of heap memory.  This limit is met quickly for a large cached two-dimensional scrolled view &#8211; note that 2,000 by 2,000 pixels times 4 bytes/pixel is 16MB.</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.GORGES.us/2010/06/android-two-dimensional-scrollview/feed/</wfw:commentRss>
		<slash:comments>32</slash:comments>
		</item>
	</channel>
</rss>

