Android 的 ObjectEditor






4.76/5 (14投票s)
ObjectEditor 作为自定义 ExpandableListView。
目录
引言
在创建一个大型应用程序时,我需要一种 .NET 开发者熟悉的类似对象编辑器。他们在 Visual Studio 中很熟悉这种编辑器,可以编辑文件、窗体、按钮等的属性...
它也类似于 Android 中的首选项编辑器。但我没有使用首选项编辑器,因为它在保存数据的方式上是硬编码的:我们希望数据能够读取并保存到我们正在编辑的对象中。
在本文中,您会经常遇到“属性”一词。正如所有 Java 开发者都知道的,Java 没有像 .NET 那样定义属性的概念。因此,如果我使用“属性”一词,我指的是 setter 和 getter 方法的组合。例如,以下表示类型为 int
的 Width
属性
private int width;
public int getWidth()
{
return width;
}
public void setWidth(int value)
{
width = value;
}
分析待编辑对象
结构
对象的属性由 PropertyProxy
类表示。这些属性由 ObjectProxy
创建,它通过 ObjectPropertyList
和 ObjectPropertyIterator
遍历对象的属性,这些类检查它们是否满足属性的要求。
- 它没有被注释为隐藏
- 它的名称以“set”开头
如果方法符合这些标准,则从 setter 创建一个 PropertyProxy
对象。然后,该对象用于设置和获取其表示的属性的值、验证值等...
代码
ObjectProxy
ObjectProxy
负责分析待编辑对象并创建可编辑的属性列表。列表的创建被委托给 ObjectPropertyIterator
类。因此,该类还负责排除 HideSetter
注释指定的特定属性。ObjectPropertyList
和 ObjectPropertyIterator
实现了一个 Java 迭代器。
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
此类抽象了 getter 和 setter 方法,以便可以通过单个对象访问它们。它由 setter 方法创建。
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 ...
}
它还具有获取和设置属性值的方法
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;
}
以及获取名称和显示名称(后者可通过 DisplayName
注释自定义)的方法。名称和显示名称在构造函数中检索。
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;
}
可以通过使用 DisplayName
注释来自定义显示名称。
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface DisplayName {
String CategoryName() default "";
String Name();
}
还有更多方法,但将在相应的章节中进行解释。
可视化待编辑对象
结构
ObjectEditorView
的基本原理是使用 Android 的 ExpandableListView
并为其提供一个适配器,该适配器生成所需的项,即待编辑对象属性的查看器和编辑器。我正在使用 ExpandableListView,因为我希望能够对待编辑对象的属性进行分类。项的生成是一个两步过程。
第一步是为对象属性使用的查看器和编辑器创建容器。有三种不同的编辑属性的方式,因此也有三种不同的容器。
- 直接在查看器中,此时查看器就是编辑器:
PropertyEditorFrame
- 在对话框内部。编辑器显示在对话框中:
PropertyEditorFrameExternalDialog
- 在另一个 Activity 中。编辑器显示在另一个 Activity 中:
PropertyEditorFrameExternalIntent
编辑器类型决定容器类型的原因是,容器负责创建编辑器对话框或启动 Activity。
第二步是为属性创建查看器并将其放入容器中。选择在容器中显示哪个查看器是基于属性类型的。
编辑器的创建被推迟到属性实际被编辑为止。
为了维护要用于特定类型的查看器或编辑器,我们有两个 TypeViewRegistry
类的实例:一个用于查看器,一个用于编辑器。默认值可以通过使用 DataTypeViewer
和 DataTypeEditor
注释来覆盖。
代码
所有这些都发生在以下类中。
ObjectEditorView
此类是一切的来源:它创建 ExpandableListView
和 ObjectProxyAdapter
来填充视图。ObjectProxyAdapter
从上面讨论的 ObjectProxy
获取数据。
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
ObjectProxyAdapter
使用 ObjectProxy
为 ObjectEditorView
创建视图。在其构造函数中,它还接收查看器和编辑器注册表。
网上已经有一些 文章 解释 了如何使用 BaseExpandableListAdapter
,所以我不会解释一般概念,只解释我如何在 ObjectEditorView
中使用它。
由于 Android 的 ExpandableListView
使用 视图回收,因此我们必须指定我们有多少种视图类型。我选择使用基于容器类型的视图回收。如上所述,我们有三种类型的容器。因此,我们定义
@Override
public int getChildTypeCount() {
return 3;
}
我们还必须告诉视图的类型。
@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;
}
接下来,我们必须创建构成 ExpandableListView
的视图。
这是在以下方法中完成的。
int getGroupCount
:ObjectProxy
的类别数。View getGroupView
:获取某个类别的视图。int getChildrenCount
:某个类别内的属性数。View getChildView
:获取某个类别内的某个属性的视图。
@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;
}
getGroupView
方法负责创建和/或填充用于显示分组的视图。如果该方法在 convertView
参数中提供了非 null 值,则表示正在使用回收,您应该填充提供的视图。反之,如果参数为 null,则您应该创建一个视图,填充它,然后返回它。填充视图很简单:只需获取 TextView
并将其文本设置为类别的名称。
@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;
}
getChildView
方法负责创建和/或填充用于可视化待编辑对象属性的视图。同样,如上在 getGroupView
中,如果该方法在 convertView
参数中提供了非 null 值,您应该用属性的查看器填充提供的值,否则您应该首先为该属性创建容器。您可以通过 groupPosition
和 childPosition
参数知道要编辑哪个属性。Android 会确保为您提供正确类型的视图,请记住我们有三种类型,只要您正确实现了 getChildTypeCount
和 getChildType
方法。
属性的查看器负责告知它是否可以编辑自己,或者,如果不能,则关联的编辑器是否必须在对话框或通过 intent 中显示。思路是只有查看器本身知道它是否可以编辑类型。(我知道有人可能认为查看器可以或不应该知道编辑器是否要在对话框或通过 intent 中显示,因此这可能会在将来发生变化,但目前就是这样工作的)。
public interface ITypeViewer {
TypeEditorType getEditorType();
// ... more methods ...
}
查看和编辑属性
结构
查看器总是支持查看某种类型的对象,例如 int
、string
等。然而,属性也有一个类型,它不一定与查看器的类型相同。这种差异通过使用类型转换器来解决:类型转换器将值从属性类型转换为查看器类型。
编辑器也一样。
编辑器还有一个属性:将用于/显示的键盘类型。您不希望用户在编辑整数时输入字符。
获取和设置属性值发生在 PropertyProxy
类中。
在创建 PropertyProxy
时,根据属性类型选择类型转换器并将其插入 PropertyProxy
。然后,转换器必须知道如何转换为查看器或编辑器的类型。这不是理想的情况,因为这实际上意味着类型转换器必须知道如何转换为任何类型:它不知道查看器的类型,理论上它可以是任何类型。
由于有三种类型的编辑器(内联、在 Dialog
中以及在另一个 Activity
中),因此有三种填充属性编辑器值的方式。
当查看器本身也是编辑器时,您在很大程度上可以自由选择如何实现。但通常,您会响应来自表示属性值的控件的某种事件,并将该值传播到属性。BooleanViewer
和 IntegerSliderViewer
是示例。
当编辑器显示在对话框中时,您的 editorview 被插入到对话框中。对话框还有一个应用按钮,点击该按钮会验证输入的值(见下文),并根据验证结果调用一个回调,该回调会将输入的值传输到正在编辑的属性。
最后,当编辑器是 Activity
时,会创建一个 Intent
来启动 Activity
。编辑的完成由结束 Activity
触发。正如所有 Android 开发者都知道的,当一个 Activity
结束时,Android 会返回到调用它的 Activity
。这里的调用 Activity
是拥有 ObjectEditorView
的 Activity
。这意味着您必须实现一个方法,该方法只需将结果转发给知道如何处理结果的 ObjectEditorView
。
代码
ITypeViewer
查看器必须实现 ITypeViewer
接口。
public interface ITypeViewer {
void setPropertyProxy(PropertyProxy propertyProxy);
PropertyProxy getPropertyProxy();
void setError(Boolean error);
TypeEditorType getEditorType();
Class getViewClass();
void setReadOnly(boolean readOnly);
}
大多数方法会很清楚。setError
方法允许发出某种错误发生的信号。此方法通常由也是编辑器的查看器实现。setReadOnly
方法用于禁止编辑属性。
ITypeEditor
编辑器必须实现 ITypeEditor
接口。
public interface ITypeEditor {
void setPropertyProxy(PropertyProxy propertyProxy);
PropertyProxy getPropertyProxy();
Object getEditorValue();
void setError(Boolean error);
Class getEditorClass();
}
ITypeEditor
具有与 ITypeViewer
类似的方法,除了 getEditorClass
,它是查看器 getViewClass
方法的编辑器对应项。
内联编辑器:BooleanViewer
下面您可以看到 BooleanViewer
。布尔值的可视化由复选框完成。如上所述,我们注册一个 OnCheckedChangeListener
以便在用户选中或取消选中复选框时得到通知,并相应地设置 PropertyProxy
的值。
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);
}
}
对话框中的编辑器
外部编辑器是通过单击查看器来启动的。这种 OnClick
处理由容器完成。
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);
}
}
完成编辑后,我们按“应用”按钮,这是 AlertDialog.BUTTON_POSITIVE
按钮。此按钮的 onClick
处理程序会验证值,如果一切正常,则最终调用设置为 objecteditor 的 OnEditingFinished
处理程序。在此方法中,我们设置 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);
}
}
为了将更改后的值传播回查看器,您的查看器必须实现 PropertyProxy.OnValueChangedListener
。例如,这是 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();
}
}
PropertyProxy
的 getValue
方法有一个参数,允许指定返回值的类型。该参数通常会用 getViewClass
方法的结果填充。getViewClass
方法返回查看器支持的类型。因此,类型转换器必须能够将属性类型转换为该类型。
编辑器是 Activity
:ColorEditorActivity
与对话框中的编辑器一样,编辑是通过单击查看器来启动的。这种 OnClick
处理由容器完成。
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);
}
}
如上所述,当编辑 Activity 结束时,我们会返回到调用 Activity,通常是显示 ObjectEditorView
的 Activity。这意味着您必须编写以下代码。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent){
view.HandleRequestResult(requestCode, resultCode, intent);
}
在 HandleRequestResult
中,来自编辑器的值被传给 PropertyProxy
。
public void HandleRequestResult(int requestCode, int resultCode,
Intent intent) {
Bundle resultValue = intent.getExtras();
String propertyName = resultValue.getString(PropertyEditorFrame.PROPERTYNAME);
EditingFinished(propertyName, resultValue);
}
为了让这一切正常工作,您必须遵守以下几点。
PropertyProxy
使用的转换器必须知道如何在Bundle
中存储和检索值。- 编辑
Activity
必须使用相同的序列化(from/toBundle
)。 - 返回结果时,编辑
Activity
必须将收到的值插入到PropertyEditorFrame.PROPERTYNAME
键中。这存储了正在编辑的属性的名称。
示例
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();
}
}
验证属性
结构
验证由三个类执行。
- 第一个类实际上是一个
Annotation
,它定义了验证的属性。例如,整数的最小值和最大值。它的名称通常以Validation
结尾,并通过IsValidationAnnotation
注释将其标识为验证定义。 - 第二个类执行实际的验证。它知道如何解释验证定义的属性。它的名称通常以
Validator
结尾,并通过用ValidatorType
注释最后一个类来“连接”到定义。 - 最后一个类知道如何将验证定义分派给其配套的验证器:
Validator
。
PropertyProxy
进行验证调用:它检查 getter 和 setter 方法上是否存在任何验证注解,检索它们并将它们转发给第 3 步的 Validator
类。
验证由编辑器触发。
代码
Annotation
:IntegerRangeValidation
该注解仅保存执行验证所需的数据。
@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 "";
}
配套验证器:IntegerRangeValidator
根据该注解,配套验证器知道如何执行验证。
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;
}
}
Validator
类知道,在传入注解时,要使用哪个验证器。
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;
}
}
最后,PropertyProxy
调用验证。
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;
}
目前只有对话框式编辑器会触发验证。
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);
}
}
});
}
});
}
}
但还有更多...
代码中还有比本文所述更多的功能。以下是概述。
支持自定义显示值
当您检查一些示例代码时,可能会注意到在创建 PropertyProxy
(在上面的代码中未显示)以及在获取和设置值时,会查询两个映射:fromDisplayMap
和 toDisplayMap
。它们提供了提供自定义值以代替实际值的可能性。这在枚举和使用表示选项的整数时非常有用。
支持属性间的依赖关系
在 ObjectProxy
构造函数的代码中,您可能会注意到以下行。
dependencyManager.RegisterProperty(propProxy);
同样,ITypeViewer
接口中的方法之一是 void setReadOnly(boolean readOnly);
。
这些允许您创建属性之间的依赖关系,其中某个属性只有在另一个属性具有特定值时才能编辑。这是通过 DependentOnPropertyValue
注释实现的。
... 还有更多待完成
还有更多工作要做。想到的一些事情:
- 用户界面的更多自定义。
- 支持 XML 属性配置。
结论
在本文中,我解释了 Android 对象编辑器的主要可能性。实际代码还有一些本文未介绍的可能性,因此我建议您下载代码并查看示例应用程序,其中包含演示该控件各种可能性的示例。
版本历史
- 版本 1.0:初始版本
- 版本 1.1:以下更改。
- 验证
- 验证定义注解现在通过
IsValidationAnnotation
注解进行标识。 - 验证定义注解通过
ValidatorType
注解标识其验证器。
- 验证定义注解现在通过
- 对象编辑器
- 允许自定义框架使用的视图:
ObjectEditorViewConfig
(视图 XML 中尚无属性)。 - 添加
ObjectProxy
而不是直接使用Object
:void setObjectToEdit(ObjectProxy objectToEdit)
。 TypeConverterRegistry
现在是独立的:标准转换器在静态创建中添加:static TypeConverterRegistry create()
。ObjectProxy
现在使用新类ObjectPropertyList
来获取对象的属性:此类抽象了检索过程。ObjectEditorView
中的类别名称和属性名称现在按字母顺序排序。ObjectEditorView
现在提供一个接口ItemCreationCallback
以允许通知框架创建过程。- 以前,新值被转发给查看器,查看器将其传播给属性,但现在已更改,属性本身会更新,并且查看器会收到此更改的通知。
IExternalEditorEvents
现在提供属性名称和新值,而不是编辑器。
- 允许自定义框架使用的视图: