Click here to Skip to main content
11,435,048 members (48,210 online)
Click here to Skip to main content

An ObjectEditor for Android

, 13 Dec 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
An ObjectEditor as a custom ExpandableListView

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 {
    
    // ... More code ...
    
    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;
        
        // ... More code ...
        
        converter = ObjectFactory.getTypeConverterForProperty(this, converterRegistry);
        
        // ... More code ...
    }
    
    // ... More code ...

}

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)
{
    // ... More code ...
    
    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();
    }
    
    // ... More code ...
    
}
 
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.

  1. Directly in the viewer in which case the viewer is the editor: PropertyEditorFrame
  2. Inside a dialog. The editor is shown inside a dialog: PropertyEditorFrameExternalDialog
  3. 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 the configuration

        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();
    
    // ... more methods ...
}

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);
    }
 
    // more code ...
    
    
    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;
        }
        
        // if our frame is recycled while editing, the object pointed to by propertyEditor will
        //    change and we will not get the correct value in the onClick handler. Same goes for
        //    the property
        final PropertyProxy property = propertyProxy;
        final View editorView =  
          ObjectFactory.getTypeEditorForPoperty(property, editorRegistry, this.getContext()); //propertyEditor;
        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  {
        
    // more code ...
    
    @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);
    }

    // more code ...
    
    public void setPropertyView(View propertyView)
    {
        super.setPropertyView(propertyView);
        
        propertyView.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        
        if(propertyProxy.getIsReadonly())
        {
            return;
        }
        
        // if our frame is recycled while editing, the object pointed to by propertyEditorType will
        //    change and we will not get the correct value in the onClick handler.
        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:

  1. 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
  2. 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.
  3. 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 {
    
    // more code ...

    @Override
    public void onClick(View propView) {
    
        // more code ... 
        
        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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Serge Desmedt
Software Developer (Senior)
Belgium Belgium
No Biography provided
Follow on   LinkedIn

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.150428.2 | Last Updated 13 Dec 2013
Article Copyright 2013 by Serge Desmedt
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid