Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
  • 5 commits
  • 11 files changed
  • 0 commit comments
  • 1 contributor
Commits on Mar 23, 2012
@rst Basic tests for IndexedSeqAdapter 05012b7
@rst Add filtering features to IndexedSeqAdapter fb91e9f
Commits on Mar 26, 2012
@rst Add PositronicImageButton class (as ImageButton with the obvious mixin). 0f998ea
Commits on Mar 27, 2012
@rst Gentler way to keep track of changes to text in a TextView or EditText.
Mixin which provides an 'onTextChanged' callback, which receives the
new text as an argument.

This is one case where the Android callback APIs have a lot of baggage;
the TextWatcher interface has three callbacks, and if you only want one, you
need to dummy up the other two.
05ab25e
@rst Sample contacts app: filter contacts by "has phone", or substring. d391979
View
BIN  sample/contacts_app/src/main/res/drawable-hdpi/ic_btn_round_minus.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  sample/contacts_app/src/main/res/drawable-hdpi/ic_btn_search.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  sample/contacts_app/src/main/res/drawable-mdpi/ic_btn_round_minus.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  sample/contacts_app/src/main/res/drawable-mdpi/ic_btn_search.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
51 sample/contacts_app/src/main/res/layout/contacts.xml
@@ -5,6 +5,57 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent">
+ <org.positronicnet.sample.contacts.ContactsFilterView
+ android:id="@+id/contacts_filter"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+ <TextView android:id="@+id/show_label"
+ android:text="Show"
+ android:textSize="16dp"
+ android:layout_centerVertical="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ <org.positronicnet.ui.PositronicButton
+ android:id="@+id/filterChoiceButton"
+ android:textSize="16dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@id/show_label"/>
+ <org.positronicnet.ui.PositronicImageButton
+ android:id="@+id/searchButton"
+ android:src="@drawable/ic_btn_search"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"/>
+ </RelativeLayout>
+
+ <LinearLayout android:id="@+id/contactsSearchBar"
+ android:visibility="gone"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="5dp">
+ <org.positronicnet.ui.PositronicEditText
+ android:id="@+id/searchString"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"/>
+ <org.positronicnet.ui.PositronicImageButton
+ android:id="@+id/endSearchButton"
+ android:src="@drawable/ic_btn_round_minus"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"/>
+ </LinearLayout>
+
+ </org.positronicnet.sample.contacts.ContactsFilterView>
+
+ <include layout="@layout/separator_bar"/>
+
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
View
6 sample/contacts_app/src/main/res/values/contacts_values.xml
@@ -5,6 +5,12 @@
<string name="loading_contacts">Loading Contacts</string>
<string name="no_contacts">No Contacts Found</string>
+ <!-- Choosing search filters -->
+
+ <string name="choose_filter">Show ...</string>
+ <string name="any_contact">All</string>
+ <string name="with_phones">All with phones</string>
+
<!-- Menu entries -->
<string name="new_contact">Add Contact</string>
View
143 sample/contacts_app/src/main/scala/ContactsActivity.scala
@@ -8,8 +8,9 @@ import org.positronicnet.orm.Actions._
import android.content.{Context, Intent}
import android.util.{AttributeSet, Log}
+import android.os.Bundle
import android.view.View
-import android.widget.{TextView, ListView, ImageView}
+import android.widget.{TextView, ListView, ImageView, LinearLayout}
import android.accounts.{AccountManager, Account}
@@ -33,25 +34,57 @@ class ContactsActivity
extends android.app.ListActivity
with PositronicActivityHelpers
with ActivityViewUtils
+ with TypedViewHolder
{
+ val listAdapter = new IndexedSeqAdapter[ Contact ](
+ IndexedSeq.empty,
+ R.layout.contact_view_row,
+ binder = ContactsActivityUiBinder )
+
onCreate {
useAppFacility( PositronicContentResolver )
useAppFacility( Res ) // stash a copy of the Resources
setContentView( R.layout.contacts )
+ setListAdapter( listAdapter )
useOptionsMenuResource( R.menu.contacts_menu )
onOptionsItemSelected( R.id.dump_contacts ){ dumpToLog }
onOptionsItemSelected( R.id.new_contact ) { newContact }
}
+ // Dealing with search state, as an element of activity state.
+
+ var searchState = new ContactsFilterState( R.string.any_contact, None )
+
+ override def createInstanceState = {
+ newSearchState( searchState ) // default values as above
+ findView( TR.contacts_filter ).setState( searchState )
+ }
+
+ override def saveInstanceState( b: Bundle ) =
+ b.putSerializable( "contact_search_state", searchState )
+
+ override def restoreInstanceState( b: Bundle ) = {
+ val newStateSlug = b.getSerializable( "contact_search_state" )
+ searchState = newStateSlug.asInstanceOf[ ContactsFilterState ]
+ newSearchState( searchState )
+ }
+
+ // Hook for changes to search state from UI
+
+ def newSearchState( state: ContactsFilterState ) = {
+ this.searchState = state
+ listAdapter.resetFilter( state.filterFunc )
+ }
+
+ // The list of contacts itself...
+
onResume {
(Contacts ? Query).onSuccess{ contacts =>
if (contacts.size > 0) {
val sortedContacts = contacts.sortBy{ _.displayNamePrimary.toLowerCase }
- setListAdapter( new IndexedSeqAdapter( sortedContacts,
- R.layout.contact_view_row,
- binder = ContactsActivityUiBinder
- ))
+ this.listAdapter.resetSeq( sortedContacts )
+ setListAdapter( this.listAdapter )
}
else {
// android package IDs don't show up in TypedResources, so...
@@ -118,3 +151,103 @@ class ContactsActivity
}
}
}
+
+case class ContactsFilterState ( filterChoice: Int,
+ searchString: Option[ String ] )
+{
+ def filterFunc: Option[ Contact => Boolean ] =
+ searchString match {
+ case None =>
+ filterChoice match {
+ case R.string.any_contact => None
+ case R.string.with_phones => Some( _.hasPhoneNumber )
+ }
+ case Some( str ) =>
+ val strLcase = str.toLowerCase
+ filterChoice match {
+ case R.string.any_contact =>
+ Some( contact => contact.lcDisplayName.contains( strLcase ))
+ case R.string.with_phones =>
+ Some( contact => contact.hasPhoneNumber &&
+ contact.lcDisplayName.contains( strLcase ))
+ }
+ }
+}
+
+class ContactsFilterView( ctx: Context, attrs: AttributeSet )
+ extends LinearLayout( ctx, attrs )
+ with TypedViewHolder
+ with WidgetUtils
+{
+ private var filterChoice: Int = R.string.any_contact
+ private val resources = ctx.getResources
+
+ override def onFinishInflate = {
+
+ super.onFinishInflate
+
+ findView( TR.filterChoiceButton ).onClick {
+ withChoiceFromDialog[Int]( R.string.choose_filter,
+ IndexedSeq( R.string.any_contact,
+ R.string.with_phones ),
+ resources.getString( _ ))
+ {
+ choice =>
+ this.filterChoice = choice
+ findView( TR.filterChoiceButton ).setText( choice )
+ android.util.Log.d( "XXX", "Set filterChoice to " + this.filterChoice )
+ searchStateChanged
+ }
+ }
+
+ findView( TR.searchButton ).onClick {
+ if ( findView( TR.contactsSearchBar ).getVisibility == View.GONE ) {
+ findView( TR.contactsSearchBar ).setVisibility( View.VISIBLE )
+ findView( TR.searchString ).setText( "" )
+ searchStateChanged
+ }
+ }
+
+ findView( TR.endSearchButton ).onClick {
+ findView( TR.contactsSearchBar ).setVisibility( View.GONE )
+ searchStateChanged
+ }
+
+ findView( TR.searchString ).onTextChanged { dummyText =>
+ searchStateChanged
+ }
+ }
+
+ def setState( state: ContactsFilterState ) = {
+
+ this.filterChoice = state.filterChoice
+
+ android.util.Log.d( "XXX", "Set filterChoice to " + state.filterChoice )
+
+ findView( TR.filterChoiceButton ).setText(
+ ctx.getResources.getString( state.filterChoice ))
+
+ state.searchString match {
+ case None =>
+ findView( TR.contactsSearchBar ).setVisibility( View.GONE )
+ case Some( str ) =>
+ findView( TR.contactsSearchBar ).setVisibility( View.VISIBLE )
+ findView( TR.searchString ).setText( str )
+ }
+ }
+
+ def getState =
+ ContactsFilterState(
+ filterChoice = this.filterChoice,
+ searchString =
+ findView( TR.contactsSearchBar ).getVisibility match {
+ case View.VISIBLE =>
+ Some( findView( TR.searchString ).getText.toString )
+ case View.GONE =>
+ None
+ })
+
+ def searchStateChanged =
+ getContext.asInstanceOf[ ContactsActivity ].newSearchState( getState )
+}
+
View
5 sample/contacts_app/src/main/scala/ContactsContent.scala
@@ -32,18 +32,23 @@ case class Contact (
val photoThumbnailUri: String = "",
val inVisibleGroup: Boolean = false,
val starred: Boolean = false,
+ val hasPhoneNumber: Boolean = false,
val customRingtone: String = "",
val sendToVoicemail: Boolean = false,
val id: RecordId[Contact] = Contacts.unsavedId
)
extends ManagedRecord with ReflectiveProperties
{
+ lazy val lcDisplayName = displayNamePrimary.toLowerCase // for filtering
+
@transient lazy val raw =
new HasMany( RawContacts,
ReflectUtils.getStatic[ String, CC.RawContacts ]("CONTACT_ID"))
+
@transient lazy val data =
new HasMany( ContactData,
ReflectUtils.getStatic[ String, CC.Data ]("CONTACT_ID"))
+
@transient lazy val photoQuery =
if (this.photoId.id != 0)
(ContactData.photos ? FindById( this.photoId ))
View
24 src/main/scala/ui/Adapters.scala
@@ -91,6 +91,24 @@ class IndexedSeqAdapter[T <: Object](protected var seq:IndexedSeq[T] = new Array
extends _root_.android.widget.BaseAdapter
{
protected var inflater: LayoutInflater = null
+ protected var filterOpt: Option[ T => Boolean ] = None
+
+ protected var realSeq = seq
+
+ private def notifySomethingChanged = {
+ this.realSeq = filterOpt match {
+ case Some( func ) => seq.filter( func )
+ case None => seq
+ }
+ notifyDataSetChanged
+ }
+
+ /** Method to change filtering */
+
+ def resetFilter( newFilterOpt: Option[ T => Boolean ] ) = {
+ this.filterOpt = newFilterOpt
+ notifySomethingChanged
+ }
/** Method to reset the sequence if a new copy was (or might have been)
* loaded off the UI thread.
@@ -98,7 +116,7 @@ class IndexedSeqAdapter[T <: Object](protected var seq:IndexedSeq[T] = new Array
def resetSeq( newSeq: IndexedSeq[T] ) = {
seq = newSeq
- notifyDataSetChanged
+ notifySomethingChanged
}
/** Get a view to use for the given position. Ordinarily delegates to the
@@ -154,7 +172,7 @@ class IndexedSeqAdapter[T <: Object](protected var seq:IndexedSeq[T] = new Array
/** Get the n'th item from the current sequence */
- def getItem(position: Int):T = seq(position)
+ def getItem(position: Int):T = realSeq(position)
/** Get the id of the n'th item from the current sequence */
@@ -162,7 +180,7 @@ class IndexedSeqAdapter[T <: Object](protected var seq:IndexedSeq[T] = new Array
/** Get number of items in the current sequence */
- def getCount = seq.size
+ def getCount = realSeq.size
}
/**
View
55 src/main/scala/ui/PositronicWidgetHelpers.scala
@@ -11,6 +11,8 @@ import _root_.android.util.Log
import _root_.android.view.KeyEvent
import _root_.android.view.View.OnKeyListener
+import scala.collection.mutable.ArrayBuffer
+
/** Mixin trait for view subclasses which provides
* "JQuery-style" event listener declarations. Fortunately, these
* don't conflict with the native API because they're alternate
@@ -95,6 +97,49 @@ trait PositronicHandlers {
}
+/** "JQuery-style" handler declarations for TextView-specific conditions. */
+
+trait PositronicTextViewHandlers extends android.widget.TextView {
+
+ private val parentTextView = this
+
+ /** Whenever the text changes, call `func` with the new string.
+ *
+ * Extreme shorthand for a common case of using TextWatchers.
+ */
+
+ def onTextChanged( func: String => Unit ) =
+ glueTextWatcher.watchers += func
+
+ private lazy val glueTextWatcher = makeGlueTextWatcher
+
+ private def makeGlueTextWatcher = {
+
+ val textWatcher = new android.text.TextWatcher {
+
+ val watchers = new ArrayBuffer[ String => Unit ]
+
+ def beforeTextChanged( s: CharSequence, start: Int,
+ count: Int, after: Int ) =
+ ()
+
+ def onTextChanged( s: CharSequence, start: Int,
+ count: Int, after: Int ) =
+ ()
+
+ def afterTextChanged( s: android.text.Editable ) = {
+ val txt = parentTextView.getText.toString
+ for ( watcher <- watchers )
+ watcher( txt )
+ }
+ }
+
+ this.addTextChangedListener( textWatcher )
+
+ textWatcher
+ }
+}
+
/** "JQuery-style" handler declarations for AdapterView-specific events. */
trait PositronicItemHandlers {
@@ -252,6 +297,14 @@ class PositronicButton( context: Context, attrs: AttributeSet = null )
extends _root_.android.widget.Button( context, attrs )
with PositronicHandlers
+/** An `android.widget.ImageButton` with [[org.positronicnet.ui.PositronicHandlers]]
+ * mixed in.
+ */
+
+class PositronicImageButton( context: Context, attrs: AttributeSet = null )
+ extends _root_.android.widget.ImageButton( context, attrs )
+ with PositronicHandlers
+
/** An `android.widget.EditText` with [[org.positronicnet.ui.PositronicHandlers]]
* mixed in.
*/
@@ -259,6 +312,7 @@ class PositronicButton( context: Context, attrs: AttributeSet = null )
class PositronicEditText( context: Context, attrs: AttributeSet = null )
extends _root_.android.widget.EditText( context, attrs )
with PositronicHandlers
+ with PositronicTextViewHandlers
/** An `android.widget.TextView` with [[org.positronicnet.ui.PositronicHandlers]]
* mixed in.
@@ -267,6 +321,7 @@ class PositronicEditText( context: Context, attrs: AttributeSet = null )
class PositronicTextView( context: Context, attrs: AttributeSet = null )
extends _root_.android.widget.TextView( context, attrs )
with PositronicHandlers
+ with PositronicTextViewHandlers
/** An `android.widget.ListView` with [[org.positronicnet.ui.PositronicHandlers]]
* and [[org.positronicnet.ui.PositronicItemHandlers]] mixed in.
View
55 src/test/scala/AdapterSpec.scala
@@ -13,6 +13,61 @@ class AdapterSpec
with ShouldMatchers
with RobolectricTests
{
+ describe("IndexedSeqAdapter") {
+
+ describe( "usage with a simple sequence" ) {
+
+ val data = IndexedSeq( "foo", "bar", "moo" )
+
+ def adapter = new IndexedSeqAdapter( data )
+
+ it ("should report count correctly") {
+ adapter.getCount should be (3)
+ }
+
+ it ("should fake item IDs correctly") {
+ adapter.getItemId(1) should be (1)
+ adapter.getItemId(2) should be (2)
+ }
+
+ it ("should retrieve items correctly") {
+ adapter.getItem(0) should be ("foo")
+ adapter.getItem(2) should be ("moo")
+ }
+ }
+
+ describe( "usage with a filter") {
+
+ case class TodoItem( description: String, isDone: Boolean )
+
+ val data = IndexedSeq( TodoItem( "feed dog", true ),
+ TodoItem( "wash dog", false ),
+ TodoItem( "walk dog", true ),
+ TodoItem( "pet dog", false ))
+
+ def adapter = {
+ val ret = new IndexedSeqAdapter( data )
+ ret.resetFilter( Some( _.isDone ))
+ ret
+ }
+
+ it ("should report count correctly") {
+ adapter.getCount should be (2)
+ }
+
+ it ("should fake item IDs correctly") {
+ adapter.getItemId(0) should be (0)
+ adapter.getItemId(1) should be (1)
+ }
+
+ it ("should retrieve items correctly") {
+ adapter.getItem(0) should be (TodoItem( "feed dog", true ))
+ adapter.getItem(1) should be (TodoItem( "walk dog", true ))
+ }
+ }
+
+ }
+
describe("IndexedSeqGroupAdapter") {
val data =

No commit comments for this range

Something went wrong with that request. Please try again.