应用程序中有一种很常见的情况是由事件数据驱动UI的显示。本教程中将展示一个我几乎每天工作中都用到的非常有用的技术,这是此前一个非常有才华的工程师教给我的。用于建立多个自定义的视图并用于不同的布局,然后将数据更新到每个布局的一个容器视图里。本教程的所有代码都可以在GitHub上面找到。
英文好且有梯子的请戳这里看原文。
在这个例子中我们要做的第一件事就是创建一个用于更新视图的接口。这个接口只有一个update
方法,用于更新视图数据。
1 2 3
| public interface Updateable { public void update( Weather weather ); }
|
接下来,我们将实现一个简单的数据模型,容纳我们用于驱动视图的所有数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Weather { private int temperature; private int windSpeed; private WindDirection windDirection; private WeatherCondition condition;
public Weather( int temperature, int windSpeed, WindDirection windDirection, WeatherCondition condition ) { this.temperature = temperature; this.windSpeed = windSpeed; this.windDirection = windDirection; this.condition = condition;
}
public void setTemperature( int temperature ) { this.temperature = temperature; }
public int getTemperature() { return temperature; } ...
|
WindDirection
和WeatherConditon
只是一些像这样的枚举类型:
1 2 3 4 5 6 7 8
| public enum WeatherCondition { RAIN, SNOW, LIGHTNING, SUN, CLOUDY, FOG }
|
现在我们已经创建好了数据模型和接口。接下来开始为我们的项目创建updateable
视图。为了开始,我们不用标准的只视图,在这里我们修改TextView和ImageView,然后将要触发更新状态的的子视图在其容器视图里面更新。我们第一个updateable视图是WeatherImage,只是简单的继承了ImageView
并实现了Updateable
接口。
1
| public class WeatherImage extends ImageView implements Updateable
|
在update
方法中,我们检查Weather
对象所准备的weatherCondition的数据,并根据数据展示一张图片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Override public void update( Weather weather ) { Log.e( "WeatherImage", "update!" ); if( weather == null || weather.getWeatherCondition() == null ) return;
switch( weather.getWeatherCondition() ) { case CLOUDY: { setImageResource( R.drawable.cloudy ); break; } case FOG: { setImageResource( R.drawable.fog ); break; } case LIGHTNING: { setImageResource( R.drawable.lightning ); break; } case RAIN: { setImageResource( R.drawable.rain ); break; } case SNOW: { setImageResource( R.drawable.snow ); break; } case SUN: { setImageResource( R.drawable.sun ); break; } } }
|
我们用同样的方法实现了WindWeatherTextView
,WeatherTextView
和WeatherTemperatureTextView
。只是把继承的ImageView改为TextView,如名字所示那样。
真正神奇的地方发生在这个为其他可更新子视图创建的容器UpdateableLinearLayout
这里。这个容器同样实现了Updateable接口,然而并不是用于调整自身,而是调用了其他子视图的update
方法。
1
| public class UpdateableLinearLayout extends LinearLayout implements Updateable
|
1 2 3 4 5 6 7 8 9
| @Override public void update( Weather weather ) { Log.e("UpdateableLinearLayout", "Update!" ); if( weather != null && mUpdateableViews != null && !mUpdateableViews.isEmpty() ) { for( Updateable view : mUpdateableViews ) { view.update( weather ); } } }
|
mUpdateableViews
是Updateable对象们的一个列表,为了创建一个完整的列表视图,它通过循环和递归找到UpdateableLinearLayout
和其子视图中所有实现了Updateable
接口的对象,并填充onFinishInflate()
方法中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @Override protected void onFinishInflate() { super.onFinishInflate(); mUpdateableViews = findTopLevelUpdateables( this ); }
public List<Updateable> findTopLevelUpdateables( ViewGroup view ) { ArrayList<Updateable> results = new ArrayList<Updateable>();
int childCount = view.getChildCount(); for( int i = 0; i < childCount; i++ ) { results = findTopLevelUpdateables( view.getChildAt( i ), results ); } return results; }
protected ArrayList<Updateable> findTopLevelUpdateables( View view, ArrayList<Updateable> results ) {
if( ( view instanceof ViewGroup ) && !( view instanceof Updateable ) ) { ViewGroup viewGroup = (ViewGroup) view; int childCount = viewGroup.getChildCount(); for( int i = 0; i < childCount; i++ ) { findTopLevelUpdateables( viewGroup.getChildAt( i ), results ); } }
Updateable result = ( view != null && view instanceof Updateable ) ? (Updateable) view : null; if( result != null ) { results.add( result ); } results.trimToSize(); return results; }
|
同样的方法可以很容易的用到RelativeLayout
或者任何其他的ViewGroup
中。
一旦我们的类都创建好了,我们可以把他们放到用户的布局文件中,如下面的activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <com.ptrprograms.eventdrivenhierarchicalviews.view.UpdateableLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/root" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
<com.ptrprograms.eventdrivenhierarchicalviews.view.WeatherImage android:layout_width="match_parent" android:layout_height="300dp" />
<com.ptrprograms.eventdrivenhierarchicalviews.view.WeatherTextView android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="24sp" />
<com.ptrprograms.eventdrivenhierarchicalviews.view.WeatherTemperatureTextView android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="24sp" />
<com.ptrprograms.eventdrivenhierarchicalviews.view.WindWeatherTextView android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="24sp" />
</com.ptrprograms.eventdrivenhierarchicalviews.view.UpdateableLinearLayout>
|
如你所视,在这个布局中,UpdateableLinearLayout
是我们的根ViewGroup,它所有的子View都是实现了Updateable
接口的。然而你也可以使用那些没有实现Updateable
的View,只是当数据改变的时候他们不会自动更新。
在这个例子中,我们使用UpdateableLinearLayout
声明该布局,并在MainActivity的onCreate
方法中初始化:
1 2
| setContentView(R.layout.activity_main); mRootView = (UpdateableLinearLayout) findViewById( R.id.root );
|
在这里,我们没有使用一个活动的API来驱动我们的数据,只是简单的创建了一个方法来模拟每三秒产生一个随机的Weather
数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private void startSimulation() { mHandler = new Handler(); Runnable runnable = new Runnable() { @Override public void run() { if( mCurrentItem >= mWeather.size() ) mCurrentItem = 0;
mRootView.update( mWeather.get( mCurrentItem++ ) );
if( mHandler != null ) mHandler.postDelayed( this, 3000 ); } };
runnable.run(); }
|
这样,我们就有个由数据驱动的自定义视图。并可以在不同的地方建立多个布局文件重复使用这些自定义视图。这个技术在我日常开发中价值不可估量,我希望它对其他的安卓开发者同样有用。