Contents
Introduction
In creating a larger application I needed a kind of object editor well known to .NET developers. They are familiar with it in Visual Studio allowing to edit the properties of files, forms, buttons, etc...
It is also similar to the preferences editor in Android. I didn't use the preferences editor however because it is hardcoded in the way it saves the data: we want the data to be read and saved to the object we are editing.
In the article you will frequently encounter the word "property". As all Java developers know Java has no concept of property like .NET has. So if I use the word "property" I mean the combination of a setter and a getter method. Following for example represents a property Width
of type int
:
private int width;
public int getWidth()
{
return width;
}
public void setWidth(int value)
{
width = value;
}
Analyzing the object to edit
The structure
An object's properties are represented by the PropertyProxy
class. These are created by the ObjectProxy
which iterates over the objects methods using the ObjectPropertyList
and ObjectPropertyIterator
which checks if they fullfill the requirements of a property:
- It is not annotated as being Hidden
- It has a name which starts with "set"
If the method complies with these criteria, then a PropertyProxy
object is created from the setter. This object is then used to set and get the value of the property it represents, to validate values, etc....
The code
ObjectProxy
The ObjectProxy
is responsible for analyzing the object to edit and to create a list of properties which can be edited. The creation of the list is forwarded to the ObjectPropertyIterator
class. And this class is thus also responsible for excluding specific properties as set by the HideSetter
annotation. The ObjectPropertyList
and the ObjectPropertyIterator
implement a Java iterator.
public class ObjectProxy {
private PropertyDependencyManager dependencyManager = new PropertyDependencyManager();
private Object anObject;
private TypeConverterRegistry converters;
private ArrayList<propertycategory> categories = new ArrayList<propertycategory>();
private ArrayList<propertyproxy> properties = new ArrayList<propertyproxy>();
public ObjectProxy(Object obj, TypeConverterRegistry converters)
{
this.converters = converters;
this.anObject = obj;
PropertyCategory defaultCategory = new PropertyCategory();
defaultCategory.setName(PropertyCategory.DefaultCategoryName);
defaultCategory.setProperties(new ArrayList<propertyproxy>());
categories.add(defaultCategory);
try {
ObjectPropertyList propertyList = new ObjectPropertyList(obj);
for(Method aMethod : propertyList)
{
PropertyProxy propProxy = PropertyProxy.CreateFromSetter(aMethod, anObject, converters);
properties.add(propProxy);
dependencyManager.RegisterProperty(propProxy);
String propertyCategory = propProxy.getCategory();
if(propertyCategory == null || propertyCategory.isEmpty())
{
defaultCategory.addProperty(propProxy);
}
else
{
PropertyCategory existingCategory = null;
for(PropertyCategory cat : categories)
{
if(cat.getName().equalsIgnoreCase(propertyCategory))
{
existingCategory = cat;
break;
}
}
if(existingCategory == null)
{
existingCategory = new PropertyCategory();
existingCategory.setName(propertyCategory);
existingCategory.setProperties(new ArrayList<propertyproxy>());
categories.add(existingCategory);
}
existingCategory.addProperty(propProxy);
}
}
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
public void Destroy()
{
for(PropertyCategory category : categories)
{
for(PropertyProxy property : category.getProperties())
{
property.removeOnValueChangedListener(dependencyManager);
}
}
}
public TypeConverterRegistry getConverters()
{
return this.converters;
}
public ArrayList<propertycategory> getCategories()
{
if(!isSorted)
{
Collections.sort(categories, new CategoryOrderingComparator());
isSorted = true;
}
return categories;
}
public PropertyProxy getProperty(String propertyName)
{
PropertyProxy result = null;
for(PropertyProxy property : properties)
{
if(property.getName().equals(propertyName))
{
return property;
}
}
return result;
}
private boolean isSorted = false;
}
public class ObjectPropertyList implements Iterable<Method> {
public ObjectPropertyList(Object obj)
{
this.obj = obj;
}
@Override
public ObjectPropertyIterator iterator() {
return new ObjectPropertyIterator(this.obj);
}
private Object obj;
}
public class ObjectPropertyIterator implements Iterator<Method> {
public ObjectPropertyIterator(Object obj)
{
this.obj = obj;
Class<? extends Object> aClass = this.obj.getClass();
hideSettersAnnotation = (HideSetters)aClass.getAnnotation(HideSetters.class);
showGettersAnnotation = (ShowGetters)aClass.getAnnotation(ShowGetters.class);
Iterable<Method> methodIterable = Arrays.asList(aClass.getMethods());
methodIterator = methodIterable.iterator();
}
@Override
public boolean hasNext() {
while(methodIterator.hasNext())
{
Method setter = methodIterator.next();
if(ValidateSetter(setter))
{
next = setter;
return true;
}
}
return false;
}
@Override
public Method next() {
return next;
}
@Override
public void remove() {
}
private boolean ValidateSetter(Method setter)
{
String methodName = setter.getName();
boolean isHidden = false;
if(hideSettersAnnotation != null)
{
for(HideSetter hideSetterAnnotation : hideSettersAnnotation.value())
{
String hideSetterName = hideSetterAnnotation.Name();
if(hideSetterName.equals(methodName))
{
isHidden = true;
break;
}
}
}
if(!methodName.startsWith("set")
|| (setter.getAnnotation(HideSetter.class) != null)
|| isHidden)
{
return false;
}
return true;
}
private Object obj;
private Iterator<Method> methodIterator;
private Method next;
HideSetters hideSettersAnnotation;
ShowGetters showGettersAnnotation;
}
PropertyProxy
This class abstracts the getter and setter methods to make them accessible from a single object. It is created from the setter method.
public class PropertyProxy implements INotifyPropertyChanged {
public static PropertyProxy CreateFomPopertyName(String propertyName,
Object obj, TypeConverterRegistry converterRegistry)
{
Class<? extends Object> aClass = obj.getClass();
Method setter = null;
try {
for (Method someMethod : aClass.getMethods())
{
if(someMethod.getName().equals("set" + propertyName))
{
setter = someMethod;
break;
}
}
} catch (SecurityException e) {
e.printStackTrace();
}
return CreateFromSetter(setter, obj, converterRegistry);
}
public static PropertyProxy CreateFromSetter(Method setter,
Object obj, TypeConverterRegistry converterRegistry)
{
String methodName = setter.getName();
String stdMethodName = methodName.substring(3);
Class<? extends Object> aClass = obj.getClass();
Method getter = null;
try {
getter = aClass.getMethod("get" + stdMethodName, null);
} catch (SecurityException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return new PropertyProxy(
setter,
getter,
obj,
converterRegistry);
}
private PropertyProxy(Method setter, Method getter,
Object obj, TypeConverterRegistry converterRegistry)
{
propertySetter = setter;
propertyGetter = getter;
theObject = obj;
converter = ObjectFactory.getTypeConverterForProperty(this, converterRegistry);
}
}
It also has methods to get and set the value of the property:
private Object getRawValue()
{
Object value = null;
try {
value = propertyGetter.invoke(theObject);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return value;
}
public Object getValue(Class toType)
{
Object displayValue = null;
if(toDisplayMap != null)
{
displayValue = toDisplayMap.get(getRawValue());
return displayValue;
}
displayValue = converter.ConvertTo(getRawValue(), toType);
return displayValue;
}
private void setRawValue(Object value)
{
try {
Object previousValue = currentValue;
propertySetter.invoke(theObject, value);
currentValue = value;
if(onValueChangedListeners != null && onValueChangedListeners.size() != 0)
{
for(OnValueChangedListener onValueChangedListener : onValueChangedListeners)
{
onValueChangedListener.ValueChanged(this, previousValue, currentValue);
}
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
public void setValue(Object value)
{
Object convertedValue = convertToRawValue(value);
setRawValue(convertedValue);
}
public Object convertToRawValue(Object value)
{
Object convertedValue = null;
if(fromDisplayMap != null)
{
convertedValue = fromDisplayMap.get(value);
return convertedValue;
}
convertedValue = converter.ConvertFrom(value);
return convertedValue;
}
And methods to get the name and display name (the last one customizable with the DisplayName
annotation). The name and display name are retrieved in the constructor:
private PropertyProxy(Method setter, Method getter, Object obj, TypeConverterRegistry converterRegistry)
{
String methodName = propertySetter.getName();
propertyName = methodName.substring(3);
propertyDisplayName = propertyName;
if(propertyGetter.isAnnotationPresent(DisplayName.class))
{
DisplayName displayName = propertyGetter.getAnnotation(DisplayName.class);
propertyDisplayName = displayName.Name();
categoryName = displayName.CategoryName();
}
else
{
Category category = propertyGetter.getAnnotation(Category.class);
if(category == null)
categoryName = null;
else
categoryName = category.Name();
}
}
public String getName()
{
return propertyName;
}
public String getDisplayName()
{
return propertyDisplayName;
}
The display name can be customized by using the DisplayName
annotation:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface DisplayName {
String CategoryName() default "";
String Name();
}
There are more methods, but these will be explained in the appropriate section.
Visualizing the object to edit
The structure
The basic principle of the ObjectEditorView
is to use the Android ExpandableListView
and provide it an adapter which produces the desired items, that is the viewers and editors for the properties of the object to edit. I'm using the ExpandableListView because I want to be able to categorize the properties of the object to edit. The generation of the items is a two step process:
Step 1 creates the holders for the viewers and editors used for the properties of the object. There are
three different ways of editing a property and as such, there are also three different holders.
- Directly in the viewer in which case the viewer is the editor:
PropertyEditorFrame
- Inside a dialog. The editor is shown inside a dialog:
PropertyEditorFrameExternalDialog
- Inside another activity. The editor is shown inside another activity:
PropertyEditorFrameExternalIntent
The reason the type of editor determines the type of holder is that the holder is responsible for the creation of the dialog for the editor or the starting of the activity.
Step 2 is the creation of the viewer for the property and putting it inside the holder. The selection of which viewer to show in the holder is based on the type of the property.
Creation of the editors is postponed until a property will effectively be edited.
To maintain which viewers or editors to use for a certain type, we have two instances of the class TypeViewRegistry
: one for the viewers, and one for the editors. The default can be overridden by using the DataTypeViewer
and DataTypeEditor
annotations.
The code
All this happens in the following classes:
ObjectEditorView
This class is the source of everything: it creates the ExpandableListView
and the ObjectProxyAdapter
to fill the view. The ObjectProxyAdapter
gets its data from the ObjectProxy
discussed above.
public class ObjectEditorView extends LinearLayout
implements IExternalEditorEvents {
public interface ItemCreationCallback
{
public void OnFrameCreated(View frame);
public void OnFrameInitialized(View frame, PropertyProxy property);
public void OnFrameUnInitialized(View frame);
}
public ObjectEditorView(Context context) {
super(context);
config = new ObjectEditorViewConfig();
Initialize(context, config);
}
private void Initialize(Context context, ObjectEditorViewConfig configuration)
{
editorRegistry = new TypeViewRegistry();
viewerRegistry = new TypeViewRegistry();
viewerRegistry.registerType(boolean.class, BooleanViewer.class);
config = configuration;
propertyListView = new ExpandableListView(context);
propertyListView.setGroupIndicator(null);
addView(propertyListView,
new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
}
public ObjectProxy getObjectToEdit()
{
return proxy;
}
public void setObjectToEdit(ObjectProxy objectToEdit)
{
this.proxy = objectToEdit;
adapter = new ObjectProxyAdapter(this, this.getContext(), proxy,
viewerRegistry, editorRegistry,
config, itemCreationCallback);
propertyListView.setAdapter(adapter);
int numberOfGroups = adapter.getGroupCount();
for (int i = 0; i < numberOfGroups; i++)
{
propertyListView.expandGroup(i);
}
}
}
ObjectProxyAdapter
The ObjectProxyAdapter
uses the ObjectProxy
to create the views for the ObjectEditorView
. In its constructor, it also receives the viewer and editor registries.
There are already some articles available on the internet which explain how to use the BaseExpandableListAdapter
so I will not explain the general concept but only how I used it in the ObjectEditorView
.
Because the Android ExpandableListView
uses view recycling, we must specify how many types of views we have. I choose to use view recycling based on the types of holders we have. Like mentioned above, we have
three types of holders. Thus we define:
@Override
public int getChildTypeCount() {
return 3;
}
We must also tell the types of the views:
@Override
public int getChildType(int groupPosition, int childPosition)
{
PropertyProxy child = (PropertyProxy) getChild(groupPosition, childPosition);
View propertyView = getViewer(child);
if(((ITypeViewer)propertyView).getEditorType() == TypeEditorType.ExternalDialog)
return 1;
if(((ITypeViewer)propertyView).getEditorType() == TypeEditorType.ExternalIntent)
return 2;
return 0;
}
Next, we must create the views that make-up the ExpandableListView
This is done in the following methods:
int getGroupCount
: The number of categories of the ObjectProxy
.View getGroupView
: Get the view for a certain category.int getChildrenCount
: The number of properties inside a certain category.View getChildView
: Get the view for a certain property inside a certain category
@Override
public int getGroupCount() {
return categories.size();
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded,
View convertView, ViewGroup parent) {
PropertyCategory category = (PropertyCategory) getGroup(groupPosition);
if (convertView == null) {
LayoutInflater inf = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inf.inflate(R.layout.expandlist_group_item, null);
}
TextView tv = (TextView) convertView.findViewById(R.id.tvGroup);
tv.setText(category.getName());
return convertView;
}
The getGroupView
method is responsible for creating and/or filling a view used to show grouping. If the method is provided with a non null value in the convertView
parameter, this means recycling is used and you should fill the provided view. If on the contrary the argument is null, then you should create a view, fill it, and then return it. Filling the view is simple: just get the TextView
and set its text to the name of the category.
@Override
public int getChildrenCount(int groupPosition) {
ArrayList<PropertyProxy> propList = categories.get(groupPosition).getProperties();
int groupSize = propList.size();
return groupSize;
}
@Override
public View getChildView(int groupPosition, int childPosition,
boolean isLastChild, View convertView, ViewGroup parent) {
PropertyProxy child = (PropertyProxy) getChild(groupPosition, childPosition);
View propertyView = null;
propertyView = getViewer(child);
if (convertView == null) {
PropertyEditorFrame frameView = null;
if(((ITypeViewer)propertyView).getEditorType() == TypeEditorType.ExternalDialog)
{
frameView = new PropertyEditorFrameExternalDialog(context, objectEditorConfig, editorRegistry);
}
else if (((ITypeViewer)propertyView).getEditorType() == TypeEditorType.ExternalIntent)
{
frameView = new PropertyEditorFrameExternalIntent(context, objectEditorConfig, editorRegistry);
}
else
{
frameView = new PropertyEditorFrame(context, objectEditorConfig);
}
if(itemCreationCallback != null)
{
itemCreationCallback.OnFrameCreated(frameView);
}
frameView.setObjectEditor(objectEditor);
convertView = frameView;
}
else
{
if(itemCreationCallback != null)
{
itemCreationCallback.OnFrameUnInitialized(convertView);
}
((PropertyEditorFrame)convertView).Clear();
}
convertView.setTag(getChildTag(groupPosition, childPosition));
((PropertyEditorFrame)convertView).setProperty(child);
if(((ITypeViewer)propertyView).getEditorType() == TypeEditorType.ExternalDialog)
{
View editor = getEditor(child);
editor.setTag(getChildTag(groupPosition, childPosition));
((PropertyEditorFrameExternalDialog)convertView).setPropertyViews(propertyView);
((PropertyEditorFrameExternalDialog)convertView).addEventWatcher(objectEditor);
}
else if (((ITypeViewer)propertyView).getEditorType() == TypeEditorType.ExternalIntent)
{
((PropertyEditorFrameExternalIntent)convertView).setPropertyViews(propertyView, getEditorClass(child));
}
else
{
((PropertyEditorFrame)convertView).setPropertyView(propertyView);
}
if(itemCreationCallback != null)
{
itemCreationCallback.OnFrameInitialized(convertView, child);
}
return convertView;
}
The getChildView
method is responsible for creating and/or filling a view used for visualizing properties of the object to edit. Again, as above in the getGroupView
, if the method is provided with a non-null value in the convertView
parameter, you should fill the provided value with the viewer for the property, else you should first create the holder for the property. You know which property to edit by the groupPosition
and childPosition
arguments. Android makes sure it provides you with the correct type of view, remember we had three types, as long as you implement the methods getChildTypeCount
and getChildType
correctly.
The viewer of a property is responsible for telling if it can edit itself or, if not, if the associated editor must be shown in a dialog or through an intent the idea being that only a viewer itself knows if it also can edit a type. (I know one can argue that the viewer can or should not know if an editor is to be shown in a dialog or through an intent and thus this might change in the future, but for now this is how it works)
public interface ITypeViewer {
TypeEditorType getEditorType();
}
Viewing and Editing properties
The structure
A viewer always has a certain type of object it supports viewing, like an int
, a string
, etc... The property however also has a certain type which is not necesarily the same as the one from the viewer. This discrepancy is solved through the use of typeconverters: the typeconverter converts the value from the type of the property to the type of the viewer
The same goes for an editor.
An editor also has another property: the type of keyboard that will be used/shown to edit the property. You wouldn't want the user to be able to enter characters when editing an integer.
Getting and setting values of a property happens inside the PropertyProxy
class.
Upon creation of the PropertyProxy
the typeconverter is selected based on the type of the property and inserted in the PropertyProxy
. The converter must then know how to convert to the type of the viewer or editor. This is not an ideal situation because this actually means the typeconverter should know how to convert to any type: it doesn't know what the type of the viewer will be and theoreticaly this could be any type.
As there are three types of editors (inline, in a Dialog
and in another Activity
), there are also three ways of filling the property with the value of the editor.
When the viewer is also the editor, you are more or less free to choose how to implement this. But typically you will respond to some event from a widget representing the value of the property and propagate the value to the property. The BooleanViewer
and IntegerSliderViewer
are examples.
When the editor is shown in a dialog, your editorview is inserted inside a dialog. The dialog also has an apply button, which when clicked, validates the entered value (see further) and depending on this validation result calls a callback which transfers the entered value to the property edited.
And finally, when the editor is an Activity
, an Intent
is created to start the Activity
. The finishing of the editing is triggered by ending the Activity
. And as all Android developers know, when an Activity
is ended Android pops back to the calling Activity
. And here the calling Activity
is the Activity
owning the ObjectEditorView
. This means YOU have to implement a method which simply forwards the result to the ObjectEditorView
which knows how to handle the result.
The code
ITypeViewer
The viewers must implement the ITypeViewer
interface:
public interface ITypeViewer {
void setPropertyProxy(PropertyProxy propertyProxy);
PropertyProxy getPropertyProxy();
void setError(Boolean error);
TypeEditorType getEditorType();
Class getViewClass();
void setReadOnly(boolean readOnly);
}
Most methods will be clear. The setError
method allows to signal that somehow an error happened. This method will typically be implemented by viewers which are also editors. The setReadOnly
method is there to disallow any editing of the property.
ITypeEditor
The editors must implement the ITypeEditor
interface:
public interface ITypeEditor {
void setPropertyProxy(PropertyProxy propertyProxy);
PropertyProxy getPropertyProxy();
Object getEditorValue();
void setError(Boolean error);
Class getEditorClass();
}
ITypeEditor
has similar methods as ITypeViewer
except for getEditorClass
which is the editor counterpart of the viewer
getViewClass
method.
Inline Editors: BooleanViewer
Below you see the BooleanViewer
. Visualization of the boolean
is done by a checkbox. As mentioned above we register an OnCheckedChangeListener
to be notified when the user checks or unchecks the checkbox and set the value of the PropertyProxy
accordingly:
public class BooleanViewer extends LinearLayout implements ITypeViewer, OnCheckedChangeListener {
private PropertyProxy propertyProxy;
private CheckBox booleanValue;
public BooleanViewer(Context context) {
super(context);
LayoutInflater inflater = LayoutInflater.from(context);
inflater.inflate(R.layout.typeviewer_boolean, this);
View booleanValueAsView = this.findViewById(R.id.cbTypeViewerBoolean);
booleanValue = (CheckBox)booleanValueAsView;
booleanValue.setOnCheckedChangeListener(this);
}
@Override
public void setPropertyProxy(PropertyProxy propPrxy) {
propertyProxy = propPrxy;
showValue();
}
@Override
public PropertyProxy getPropertyProxy() {
return propertyProxy;
}
@Override
public TypeEditorType getEditorType() {
return TypeEditorType.ViewIsEditor;
}
@Override
public void setError(Boolean error) {
}
private void showValue()
{
showValue((Boolean)propertyProxy.getValue(getViewClass()));
}
private void showValue(boolean value)
{
booleanValue.setChecked(value);
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if(propertyProxy != null)
{
propertyProxy.setValue(isChecked);
}
}
@Override
public Class getViewClass() {
return boolean.class;
}
@Override
public void setReadOnly(boolean readOnly) {
booleanValue.setEnabled(!readOnly);
}
}
Editor in dialog
External editors are initiated by clicking on the viewer. This OnClick
handling is done by the holder:
public class PropertyEditorFrameExternalDialog
extends PropertyEditorFrame
implements OnClickListener {
private List<iexternaleditorevents> editorEvents = new ArrayList<iexternaleditorevents>();
private TypeViewRegistry editorRegistry;
public PropertyEditorFrameExternalDialog(Context context,
ObjectEditorViewConfig configuration, TypeViewRegistry editorRegistry) {
super(context, configuration);
this.editorRegistry = editorRegistry;
Initialize(context);
}
public void setPropertyView(View propertyView)
{
super.setPropertyView(propertyView);
propertyView.setOnClickListener(this);
}
public void addEventWatcher(IExternalEditorEvents editorEvents)
{
this.editorEvents.add(editorEvents);
}
public void removeEventWatcher(IExternalEditorEvents editorEvents)
{
this.editorEvents.remove(editorEvents);
}
@Override
public void onClick(View propView) {
if(propertyProxy.getIsReadonly())
{
return;
}
final PropertyProxy property = propertyProxy;
final View editorView =
ObjectFactory.getTypeEditorForPoperty(property, editorRegistry, this.getContext());
if((editorView != null) && (propertyProxy != null))
{
((ITypeEditor)editorView).setPropertyProxy(propertyProxy);
}
final List<iexternaleditorevents> events = new ArrayList<iexternaleditorevents>(editorEvents);
if(IKeyBoardInput.class.isAssignableFrom(editorView.getClass()))
{
((IKeyBoardInput)editorView).setInputType(propertyProxy.getInputType());
}
if (editorView.getParent() != null && editorView.getParent() instanceof ViewGroup)
{
((ViewGroup)editorView.getParent()).removeAllViews();
}
if(events != null && events.size() != 0)
{
for (IExternalEditorEvents editorEvent : events)
{
editorEvent.EditingStarted(property.getName());
}
}
AlertDialog.Builder alertDialogBuilder =
new AlertDialog.Builder(PropertyEditorFrameExternalDialog.this.getContext());
alertDialogBuilder.setView(editorView);
alertDialogBuilder.setPositiveButton("Apply", null);
AlertDialog dialog = alertDialogBuilder.create();
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(final DialogInterface dialog) {
Button b = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Object value = ((ITypeEditor)editorView).getEditorValue();
if(property.validateValue(value))
{
dialog.dismiss();
if(events != null && events.size() != 0)
{
for (IExternalEditorEvents editorEvent : events)
{
editorEvent.EditingFinished(property.getName(), value);
}
}
}
else
{
((ITypeEditor)editorView).setError(true);
}
}
});
}
});
WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
lp.copyFrom(dialog.getWindow().getAttributes());
lp.width = WindowManager.LayoutParams.FILL_PARENT;
lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
dialog.show();
dialog.getWindow().setAttributes(lp);
}
}
When we've finished editing we press the "Apply" button, which is the AlertDialog.BUTTON_POSITIVE
buton. The onClick
handler
of this button validates the value and if everything is ok eventually calls the OnEditingFinished
handler which was set to the objecteditor. Inside this method
we set the value of the PropertyProxy
.
public class ObjectEditorView extends LinearLayout
implements OnDismissListener,
PropertyEditorFrameExternalDialog.OnEditingFinished,
IExternalEditorEvents {
@Override
public void EditingFinished(String propertyName, Object newValue) {
if(proxy == null)
return;
PropertyProxy property = proxy.getProperty(propertyName);
if(property == null)
return;
property.setValue(newValue);
}
}
To propagate the changed value back to the viewer, your viewer must implement PropertyProxy.OnValueChangedListener
. As an example, here is the code from DefaultViewer
public class DefaultViewer extends LinearLayout implements ITypeViewer, OnValueChangedListener {
private PropertyProxy propertyProxy;
private TextView editText;
public DefaultViewer(Context context) {
super(context);
LayoutInflater inflater = LayoutInflater.from(context);
inflater.inflate(R.layout.typeviewer_default, this);
editText = (TextView)this.findViewById(R.id.tvText);
}
@Override
public void setPropertyProxy(PropertyProxy propPrxy) {
if(propertyProxy != null)
{
propertyProxy.removeOnValueChangedListener(this);
}
propertyProxy = propPrxy;
showValue();
propertyProxy.addOnValueChangedListener(this);
}
@Override
public PropertyProxy getPropertyProxy() {
return propertyProxy;
}
private void showValue()
{
showValue((String)propertyProxy.getValue(getViewClass()));
}
private void showValue(String value)
{
if(value == null)
{
editText.setText("");
return;
}
editText.setText(value);
}
@Override
public void setError(Boolean error) {
editText.setBackgroundColor(Color.RED);
}
@Override
public TypeEditorType getEditorType() {
return TypeEditorType.ExternalDialog;
}
@Override
public Class getViewClass() {
return String.class;
}
@Override
public void setReadOnly(boolean readOnly) {
}
@Override
public void ValueChanged(PropertyProxy poperty, Object oldValue,
Object newValue) {
showValue();
}
}
The getValue
method of PropertyProxy
has an argument allowing to specify the type of the value returned. The argument will typically be filled
with the result of the getViewClass
method. The getViewClass
method returns the type which is supported by the viewer. It is thus the type to which
the typeconverter of the PropertyProxy
must be able to convert the propertytype.
Editor is Activity
: ColorEditorActivity
As with editors in a dialog, the editing is initiated by clicking on the viewer. This OnClick
handling is done by the holder:
public class PropertyEditorFrameExternalIntent
extends PropertyEditorFrame
implements OnClickListener {
public PropertyEditorFrameExternalIntent(Context context,
ObjectEditorViewConfig configuration, TypeViewRegistry editorRegistry) {
super(context, configuration);
this.editorRegistry = editorRegistry;
Initialize(context);
}
public void setPropertyView(View propertyView)
{
super.setPropertyView(propertyView);
propertyView.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if(propertyProxy.getIsReadonly())
{
return;
}
final Class propertyEditorType = ObjectFactory.getTypeEditorClassForProperty(propertyProxy, editorRegistry);
Bundle valueData = (Bundle)((ITypeViewer)propertyViewer).getPropertyProxy().getValue(Bundle.class);
Bundle arg = new Bundle();
arg.putString(PropertyEditorFrame.PROPERTYNAME, propertyProxy.getName());
arg.putAll(valueData);
Activity activity = (Activity)PropertyEditorFrameExternalIntent.this.getContext();
Intent myIntent = new Intent(activity, propertyEditorType);
myIntent.putExtras(arg);
activity.startActivityForResult(myIntent, PropertyEditorFrameExternalIntent.PROCESS_RESULT);
}
}
As mentioned above, when the editing activity is ended we return to the calling activity, typically the one showing the ObjectEditorView
. This means YOU will have to write following code:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent){
view.HandleRequestResult(requestCode, resultCode, intent);
}
Inside the HandleRequestResult
the value from the editor is handed to the PropertyProxy
:
public void HandleRequestResult(int requestCode, int resultCode,
Intent intent) {
Bundle resultValue = intent.getExtras();
String propertyName = resultValue.getString(PropertyEditorFrame.PROPERTYNAME);
EditingFinished(propertyName, resultValue);
}
For all this to work, there are a few things you must adhere to:
- The converter used by the
PropertyProxy
must know how to store and retrieve the value in and from a Bundle
- The editing
Activity
must use the same serialization from and into a Bundle
- On returning the result, the editing
Activity
must insert the value it received in the PropertyEditorFrame.PROPERTYNAME
key. This stores the name of the property which was edited.
An example:
public class ColorEditorActivity extends Activity {
private String propertyName;
private ColorEditor colorEditor;
private IntTypeConverter converter = new IntTypeConverter();
private int value;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
colorEditor = new ColorEditor(this);
this.setContentView(colorEditor);
Bundle args = getIntent().getExtras();
propertyName = args.getString(PropertyEditorFrame.PROPERTYNAME);
value = (Integer)converter.ConvertFrom(args);
colorEditor.setValue(value);
}
@Override
public void onBackPressed() {
Intent result = new Intent();
Bundle ret = new Bundle();
ret.putString(PropertyEditorFrame.PROPERTYNAME, propertyName);
Bundle colorValue = (Bundle) converter.ConvertTo(colorEditor.getValue(), Bundle.class);
ret.putAll(colorValue);
result.putExtras(ret);
setResult(Activity.RESULT_OK, result);
super.onBackPressed();
}
}
Validating properties
The structure
Validation is performed by three classes:
- A first class is actually an
Annotation
which defines the properties of the validation. For example the min and max value of an integer. Its name typically ends with Validation
and it is identified as being a validation definition by the IsValidationAnnotation
annotation - A second class does the actual validation. It knows how to interpret the properties of the validation definition. Its name typicaly ends with
Validator
and is "connected" to the definition by annotating this last one with the ValidatorType
annotation. - A last class knows how to dispatch a validation definition to its accompaning validator:
Validator
The validation call is made by PropertyProxy
: it checks if any validation annotations are present on the getter and setter method, retrieves them and forwards them to de Validator
class of step 3
The validation is triggered by the editor.
The code
The Annotation
: IntegerRangeValidation
The annotation simply holds the data needed to perform the validation.
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@IsValidationAnnotation()
@ValidatorType(Type = IntegerRangeValidator.class)
public @interface IntegerRangeValidation {
int MinValue();
int MaxValue();
boolean MinIncluded() default true;
boolean MaxIncluded() default true;
String ErrorMessage() default "";
}
The accompanying validator: IntegerRangeValidator
The accompanying validator knows, based on that annotation, how to perform the validation:
public class IntegerRangeValidator implements IValueValidator {
@Override
public boolean Validate(Object value, Annotation validationDefinition) {
IntegerRangeValidation integerRangeDefinition = (IntegerRangeValidation)validationDefinition;
int integerValue = (Integer)value;
return Validate(integerValue,
integerRangeDefinition.MinIncluded(), integerRangeDefinition.MinValue(),
integerRangeDefinition.MaxIncluded(), integerRangeDefinition.MaxValue());
}
private boolean Validate(int integerValue, boolean minIncluded,
int minValue, boolean maxIncluded, int maxValue)
{
if(((minIncluded && (minValue <= integerValue))
|| (!minIncluded && (minValue < integerValue)))
&& ((maxIncluded && (maxValue >= integerValue))
|| (!maxIncluded && (maxValue > integerValue))))
{
return true;
}
return false;
}
}
The Validator
class knows, when handed an annotation, which validator to use.
public class Validator {
public static boolean isValidationAnnotation(Annotation annotation)
{
Class<? extends Annotation> annotationType = annotation.annotationType();
Annotation isValidationAnnotation = annotationType.getAnnotation(IsValidationAnnotation.class);
if(isValidationAnnotation == null)
return false;
return true;
}
public static boolean Validate (Object value, Annotation validationDefinition)
{
IValueValidator validator = getValidator(validationDefinition);
if(validator == null)
return false;
return validator.Validate(value, validationDefinition);
}
private static IValueValidator getValidator(Annotation validationDefinition)
{
Class<? extends Annotation> annotationType = validationDefinition.annotationType();
ValidatorType validatorTypeAnnotation = (ValidatorType)annotationType.getAnnotation(ValidatorType.class);
if(validatorTypeAnnotation == null)
return null;
return instantiateValueValidatorFromClass(validatorTypeAnnotation.Type());
}
private static IValueValidator instantiateValueValidatorFromClass(Class<?> valueValidatorType)
{
Constructor<?> cons;
IValueValidator valueValidator = null;
try {
cons = valueValidatorType.getConstructor();
valueValidator = (IValueValidator)cons.newInstance();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return valueValidator;
}
}
Finally, the PropertyProxy
calls the validation:
public boolean validateValue(Object value)
{
Object convertedValue = convertToRawValue(value);
Annotation validation = GetValidation();
if(validation != null)
{
return Validator.Validate(convertedValue, validation);
}
return true;
}
public Annotation GetValidation()
{
for(Annotation annotation : propertySetter.getAnnotations())
{
if(Validator.isValidationAnnotation(annotation))
{
return annotation;
}
}
return null;
}
public Class<?> getValidationType()
{
Annotation validation = GetValidation();
if(validation != null)
{
return validation.annotationType();
}
return null;
}
Currently only the dialog style of editor triggers validation.
public class PropertyEditorFrameExternalDialog
extends PropertyEditorFrame
implements OnClickListener {
@Override
public void onClick(View propView) {
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(final DialogInterface dialog) {
Button b = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Object value = ((ITypeEditor)editorView).getEditorValue();
if(property.validateValue(value))
{
dialog.dismiss();
if(events != null && events.size() != 0)
{
for (IExternalEditorEvents editorEvent : events)
{
editorEvent.EditingFinished(property.getName(), value);
}
}
}
else
{
((ITypeEditor)editorView).setError(true);
}
}
});
}
});
}
}
But there is more ...
There is more functionality in the code then described in this article. Here's an overview:
Support for custom display values
You may have noticed when examining some of the sample code that during the construction of the PropertyProxy
(not shown in the code above) and while getting and setting the value two maps are consulted: fromDisplayMap
and toDisplayMap
. They provide the possibility to provide customized values to be shown instead of the real values. This can be handy with enumerations and with the use of integers representing options.
Support for dependencies between properties
In the code of the ObjectProxy
constructor you may have noticed following line:
dependencyManager.RegisterProperty(propProxy);
And also, one of the methods in the ITypeViewer
interface is the void setReadOnly(boolean readOnly);
method.
What these allow you is to create dependencies between properties in which a certain property can only be edited if another property has a certain value. This is done by means of the DependentOnPropertyValue
annotation.
... and more to be done
And there is more to be done. Few things that come to mind:
- More customization of the user interface
- Support for configuration by xml attributes
Conclusion
In this article I explained the main possibilities of an object editor for Android. The actual code has some more possibilities not explained in this article so I suggest you download the code and have a look at the sample application which includes samples demonstrating the various possibilities of the control.
Version history
- Version 1.0: Initial version
- Version 1.1: Following changes:
- Validation
- Validation definition annotations are now identified by the
IsValidationAnnotation
annotation - Validation definition annotations identify their validator by the
ValidatorType
annotation
- Object editor
- Allow customization of used views for frames:
ObjectEditorViewConfig
(no attributes in the view xml yet) - Add
ObjectProxy
instead of Object
directly: void setObjectToEdit(ObjectProxy objectToEdit)
TypeConverterRegistry
is now self containing: standard converters are added in a static create: static TypeConverterRegistry create()
ObjectProxy
now uses the new class ObjectPropertyList
to get the methods of an object: this class abstracts the retrieval process - Category names and Property names are now alphabetically sorted in the
ObjectEditorView
ObjectEditorView
now provides an interface ItemCreationCallback
to allow notification of the frames creation process - New values used to be forwarded to the viewer which would propagate them to the property, but this changed and now the property itself is updated and the viewers are notified of this change.
IExternalEditorEvents
now provides the property name and the new value and no longer the editor.