我正在开发一款在时间线上显示工作时间表的应用。
这是目前应用程序设计的粗略布局:
数据存储在 SQLite 数据库中。当 Timeline(一个单例对象)从数据库助手类请求数据时,它会得到一个包含 Event 的 ArrayList(例如,一个 Event 可以是从 2016 年 5 月 1 日 03:00 开始到 2016 年 5 月 3 日 16:00 结束的职责)。 Timeline 然后将这些 Event 转换为 TimelineItem,一个表示(部分)Event 的类特定的一天。
Event 的加载和 Event 到 TimelineItem 的转换都在 AsyncTasks 中完成。到目前为止一切顺利。
现在是我正在苦苦挣扎的部分:在获取新的数据库后更新 UI。
我的第一种方法是将更新的 TimelineItems 的 ArrayList 传递给 RecyclerView 适配器,并让适配器知道数据已通过 notifyDatasetChanged() 更改。这种方法的问题是
1) 正在做很多不必要的工作(因为我们正在重新计算所有事件/时间线项目,而不仅仅是那些改变的)和
2) RecyclerView 上的滚动位置在每次 DB 获取后重置
在我的第二种方法中,我实现了一些方法来检查自上次显示以来哪些事件/TimelineItems 发生了变化,其想法是使用 notifyItemChanged() 仅更改那些 TimelineItems。完成的工作更少,根本无需担心滚动位置。棘手的一点是检查哪些项目已更改确实需要一些时间,因此也需要异步完成:
我尝试在 doInBackground() 中进行代码操作,并通过在 onProgressUpdate() 中发布奥托总线事件来更新 UI。
private class InsertEventsTask extends AsyncTask<Void, Integer, Void> {
@Override
protected Void doInBackground(Void... params) {
ArrayList<Event> events = mCachedEvents;
// if mChangedEvents is not null and not empty
if (events != null && !events.isEmpty()) {
// get the list of pairs for the events
ArrayList<TimelineItemForDateTimePair> listOfPairs = convertEventsToPairs(events);
// insert the TimelineItems from the pairs into the Timeline
for (int i = 0; i < listOfPairs.size(); i++) {
// get the last position for the DateTime associated with the pair
int position = findLastPositionForDate(listOfPairs.get(i).dateTime);
// if position is -1, the events started on a day before the timeline starts
// so keep skipping pairs until position > -1
if (position > -1) {
// if the item is a PlaceholderItem
if (mTimelineItems.get(position).isPlaceholderItem) {
// remove the PlaceholderItem
mTimelineItems.remove(position);
// and add the TimelineItem from the pair at the position the PlaceholderItem was at
mTimelineItems.add(position, listOfPairs.get(i).timelineItem);
// update the UI on the UI thread
publishProgress(position, TYPE_CHANGED);
} else { // if the item is not a PlaceholderItem, there's already an normal TimelineItem in place
// place the new item at the next position on the Timeline
mTimelineItems.add(position + 1, listOfPairs.get(i).timelineItem);
publishProgress(position, TYPE_ADDED);
}
}
}
}
return null;
}
/**
* onProgressUpdate handles the UI changes on the UI thread for us. Type int available:
* - TYPE_CHANGED
* - TYPE_ADDED
* - TYPE_DELETED
*
* @param values value[0] is the position as <code>int</code>,
* value[1] is the type of manipulation as <code>int</code>
*/
@Override
protected void onProgressUpdate(Integer... values) {
int position = values[0];
int type = values[1];
// update the UI for each changed/added/deleted TimelineItem
if (type == TYPE_CHANGED) {
BusProvider.getInstance().post(new TimelineItemChangedNotification(position));
} else if (type == TYPE_ADDED) {
BusProvider.getInstance().post((new TimelineItemAddedNotification(position)));
} else if (type == TYPE_DELETED) {
// TODO: make delete work bro!
}
}
}
问题是,不知何故,在发布此进度时滚动会完全弄乱 UI。
我的主要问题是:当我更新适配器的数据集 (TimelineItems) 中的特定项目时,notifyItemChanged() 确实更改了项目但没有将项目放在正确的位置。
这是我的适配器:
/**
* A custom RecyclerView Adapter to display a Timeline in a TimelineFragment.
*/
public class TimelineAdapter extends RecyclerView.Adapter<TimelineAdapter.TimelineItemViewHolder> {
/*************
* VARIABLES *
*************/
private ArrayList<TimelineItem> mTimelineItems;
/****************
* CONSTRUCTORS *
****************/
/**
* Constructor with <code>ArrayList<TimelineItem></code> as data set argument.
*
* @param timelineItems ArrayList with TimelineItems to display
*/
public TimelineAdapter(ArrayList<TimelineItem> timelineItems) {
this.mTimelineItems = timelineItems;
}
// Create new views (invoked by the layout manager)
@Override
public TimelineItemViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
// create a new view
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_timeline, parent, false);
// set the view's size, margins, paddings and layout parameters
// ...
return new TimelineItemViewHolder(v);
}
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(TimelineItemViewHolder holder, int position) {
// - get element from your data set at this position
// - replace the contents of the view with that element
// if the item is a ShowPreviousMonthsItem, set the showPreviousMonthsText accordingly
if (mTimelineItems.get(position).isShowPreviousMonthsItem) {
holder.showPreviousMonthsText.setText(mTimelineItems.get(position).showPreviousMonthsText);
} else { // otherwise set the showPreviousMonthsText blank
holder.showPreviousMonthsText.setText("");
}
// day of month & day of week of the TimelineItem
if (mTimelineItems.get(position).isFirstItemOfDay) {
holder.dayOfWeek.setText(mTimelineItems.get(position).dayOfWeek);
holder.dayOfMonth.setText(mTimelineItems.get(position).dayOfMonth);
} else {
holder.dayOfWeek.setText("");
holder.dayOfMonth.setText("");
}
// Event name for the TimelineItem
holder.name.setText(mTimelineItems.get(position).name);
// place and goingTo of the TimelineItem
// if combinedPlace == ""
if(mTimelineItems.get(position).combinedPlace.equals("")) {
if (mTimelineItems.get(position).isFirstDayOfEvent) {
holder.place.setText(mTimelineItems.get(position).place);
} else {
holder.place.setText("");
}
if (mTimelineItems.get(position).isLastDayOfEvent) {
holder.goingTo.setText(mTimelineItems.get(position).goingTo);
} else {
holder.goingTo.setText("");
}
holder.combinedPlace.setText("");
} else {
holder.place.setText("");
holder.goingTo.setText("");
holder.combinedPlace.setText(mTimelineItems.get(position).combinedPlace);
}
if(mTimelineItems.get(position).startDateTime != null) {
holder.startTime.setText(mTimelineItems.get(position).startDateTime.toString("HH:mm"));
} else {
holder.startTime.setText("");
}
if(mTimelineItems.get(position).endDateTime != null) {
holder.endTime.setText(mTimelineItems.get(position).endDateTime.toString("HH:mm"));
} else {
holder.endTime.setText("");
}
if (!mTimelineItems.get(position).isShowPreviousMonthsItem) {
if (mTimelineItems.get(position).date.getDayOfWeek() == DateTimeConstants.SUNDAY) {
holder.dayOfWeek.setTextColor(Color.RED);
holder.dayOfMonth.setTextColor(Color.RED);
} else {
holder.dayOfWeek.setTypeface(null, Typeface.NORMAL);
holder.dayOfMonth.setTypeface(null, Typeface.NORMAL);
holder.dayOfWeek.setTextColor(Color.GRAY);
holder.dayOfMonth.setTextColor(Color.GRAY);
}
} else {
((RelativeLayout) holder.dayOfWeek.getParent()).setBackgroundColor(Color.WHITE);
}
holder.bindTimelineItem(mTimelineItems.get(position));
}
// Return the size of the data set (invoked by the layout manager)
@Override
public int getItemCount() {
return mTimelineItems.size();
}
// replace the data set
public void setTimelineItems(ArrayList<TimelineItem> timelineItems) {
this.mTimelineItems = timelineItems;
}
// replace an item in the data set
public void swapTimelineItemAtPosition(TimelineItem item, int position) {
mTimelineItems.remove(position);
mTimelineItems.add(position, item);
notifyItemChanged(position);
}
// the ViewHolder class containing the relevant views,
// also binds the Timeline item itself to handle onClick events
public class TimelineItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
protected TextView dayOfWeek;
protected TextView dayOfMonth;
protected TextView showPreviousMonthsText;
protected TextView name;
protected TextView place;
protected TextView combinedPlace;
protected TextView goingTo;
protected TextView startTime;
protected TextView endTime;
protected TimelineItem timelineItem;
public TimelineItemViewHolder(View view) {
super(view);
view.setOnClickListener(this);
this.dayOfWeek = (TextView) view.findViewById(R.id.day_of_week);
this.dayOfMonth = (TextView) view.findViewById(R.id.day_of_month);
this.showPreviousMonthsText = (TextView) view.findViewById(R.id.load_previous_data);
this.name = (TextView) view.findViewById(R.id.name);
this.place = (TextView) view.findViewById(R.id.place);
this.combinedPlace = (TextView) view.findViewById(R.id.combined_place);
this.goingTo = (TextView) view.findViewById(R.id.going_to);
this.startTime = (TextView) view.findViewById(R.id.start_time);
this.endTime = (TextView) view.findViewById(R.id.end_time);
}
public void bindTimelineItem(TimelineItem item) {
timelineItem = item;
}
// handles the onClick of a TimelineItem
@Override
public void onClick(View v) {
// if the TimelineItem is a ShowPreviousMonthsItem
if (timelineItem.isShowPreviousMonthsItem) {
BusProvider.getInstance().post(new ShowPreviousMonthsRequest());
}
// if the TimelineItem is a PlaceholderItem
else if (timelineItem.isPlaceholderItem) {
Toast.makeText(v.getContext(), "(no details)", Toast.LENGTH_SHORT).show();
}
// else the TimelineItem is an actual event
else {
Toast.makeText(v.getContext(), "eventId = " + timelineItem.eventId, Toast.LENGTH_SHORT).show();
}
}
}
这是在事件总线上发布更改时在 TimelineFragment 中触发的方法:
@Subscribe
public void onTimelineItemChanged(TimelineItemChangedNotification notification) {
int position = notification.position;
Log.d(TAG, "TimelineItemChanged detected for position " + position);
mAdapter.swapTimelineItemAtPosition(mTimeline.mTimelineItems.get(position), position);
mAdapter.notifyItemChanged(position);
Log.d(TAG, "Item for position " + position + " swapped");
}
需要注意的是,在我滚动离开更改的数据足够远并返回到之后的位置后,适配器的数据集似乎正确显示。不过,最初 UI 完全乱七八糟。
编辑:
我发现添加
mAdapter.notifyItemRangeChanged(position, mAdapter.getItemCount());
解决了这个问题,但是 - 不幸的是 - 将滚动位置设置为正在更改的位置 :(
这是我的 TimelineFragment:
/**
* Fragment displaying a Timeline using a RecyclerView
*/
public class TimelineFragment extends BackHandledFragment {
// DEBUG flag and TAG
private static final boolean DEBUG = false;
private static final String TAG = TimelineFragment.class.getSimpleName();
// variables
protected RecyclerView mRecyclerView;
protected TimelineAdapter mAdapter;
protected LinearLayoutManager mLinearLayoutManager;
protected Timeline mTimeline;
protected MenuItem mMenuItemScroll2Today;
protected MenuItem mMenuItemReload;
protected String mToolbarTitle;
// TODO: get the value of this boolean from the shared preferences
private boolean mUseTimelineItemDividers = true;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// get a handle to the app's Timeline singleton
mTimeline = Timeline.getInstance();
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
rootView.setTag(TAG);
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.timeline_list);
mRecyclerView.hasFixedSize();
// LinearLayoutManager constructor
mLinearLayoutManager = new LinearLayoutManager(getActivity());
// set the layout manager
setRecyclerViewLayoutManager();
// adapter constructor
mAdapter = new TimelineAdapter(mTimeline.mTimelineItems);
// set the adapter for the RecyclerView.
mRecyclerView.setAdapter(mAdapter);
// add lines between the different items if using them
if (mUseTimelineItemDividers) {
RecyclerView.ItemDecoration itemDecoration =
new TimelineItemDivider(this.getContext());
mRecyclerView.addItemDecoration(itemDecoration);
}
// add the onScrollListener
mRecyclerView.addOnScrollListener(new TimelineOnScrollListener(mLinearLayoutManager) {
// when the first visible item on the Timeline changes,
// adjust the Toolbar title accordingly
@Override
public void onFirstVisibleItemChanged(int position) {
mTimeline.mCurrentScrollPosition = position;
try {
String title = mTimeline.mTimelineItems
.get(position).date
.toString(TimelineConfig.TOOLBAR_DATE_FORMAT);
// if mToolbarTitle is null, set it to the new title and post on bus
if (mToolbarTitle == null) {
if (DEBUG)
Log.d(TAG, "mToolbarTitle is null - posting new title request on bus: " + title);
mToolbarTitle = title;
BusProvider.getInstance().post(new ChangeToolbarTitleRequest(mToolbarTitle));
} else { // if mToolbarTitle is not null
// only post on the bus if the new title is different from the previous one
if (!title.equals(mToolbarTitle)) {
if (DEBUG)
Log.d(TAG, "mToolbarTitle is NOT null, but new title detected - posting new title request on bus: " + title);
mToolbarTitle = title;
BusProvider.getInstance().post(new ChangeToolbarTitleRequest(mToolbarTitle));
}
}
} catch (NullPointerException e) {
// if the onFirstVisibleItemChanged is called on a "ShowPreviousMonthsItem",
// leave the title as it is
}
}
});
return rootView;
}
/**
* Set RecyclerView's LayoutManager to the one given.
*/
public void setRecyclerViewLayoutManager() {
int scrollPosition;
// If a layout manager has already been set, get current scroll position.
if (mRecyclerView.getLayoutManager() != null) {
scrollPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager())
.findFirstCompletelyVisibleItemPosition();
} else {
scrollPosition = mTimeline.mFirstPositionForToday;
}
mRecyclerView.setLayoutManager(mLinearLayoutManager);
mLinearLayoutManager.scrollToPositionWithOffset(scrollPosition, 0);
}
// set additional menu items for the Timeline fragment
@Override
public void onPrepareOptionsMenu(Menu menu) {
// scroll to today
mMenuItemScroll2Today = menu.findItem(R.id.action_scroll2today);
mMenuItemScroll2Today.setVisible(true);
mMenuItemScroll2Today.setIcon(Timeline.getIconForDateTime(new DateTime()));
mMenuItemScroll2Today.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
// stop scrolling
mRecyclerView.stopScroll();
// get today's position
int todaysPosition = mTimeline.mFirstPositionForToday;
// scroll to today's position
mLinearLayoutManager.scrollToPositionWithOffset(todaysPosition, 0);
return false;
}
});
// reload data from Hacklberry
mMenuItemReload = menu.findItem(R.id.action_reload_from_hacklberry);
mMenuItemReload.setVisible(true);
mMenuItemReload.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
// stop scrolling
mRecyclerView.stopScroll();
//
mTimeline.reloadDBForCurrentMonth();
mTimeline.loadEventsFromUninfinityDBAsync(mTimeline.mTimelineStart, mTimeline.mTimelineEnd);
return false;
}
});
super.onPrepareOptionsMenu(menu);
}
@Override
public void onResume() {
super.onResume();
// if the Timeline has been invalidated, let AllInOneActivity know it needs to replace
// this Fragment with a new one
if (mTimeline.isInvalidated()) {
Log.d(TAG, "posting TimelineInvalidatedNotification on the bus ...");
BusProvider.getInstance().post(
new TimelineInvalidatedNotification());
}
// fetch today's menu icon
if (mMenuItemScroll2Today != null) {
if (DEBUG) Log.d(TAG, "fetching scroll2today menu icon");
mMenuItemScroll2Today.setIcon(Timeline.getIconForDateTime(new DateTime()));
}
}
// from BackHandledFragment
@Override
public String getTagText() {
return TAG;
}
// from BackHandledFragment
@Override
public boolean onBackPressed() {
return false;
}
@Subscribe
public void onHacklberryReloaded(HacklberryLoadedNotification notification) {
resetReloading();
}
// handles ShowPreviousMonthsRequests posted on the bus by the TimelineAdapter's ShowPreviousMonthsItem onClick()
@Subscribe
public void onShowPreviousMonthsRequest(ShowPreviousMonthsRequest request) {
// create an empty OnItemTouchListener to prevent the user from manipulating
// the RecyclerView while it loads more data (would mess up the scroll position)
EmptyOnItemTouchListener listener = new EmptyOnItemTouchListener();
// add it to the RecyclerView
mRecyclerView.addOnItemTouchListener(listener);
// load the previous months (= add the required TimelineItems)
int newScrollToPosition = mTimeline.showPreviousMonths();
// pass the new data set to the TimelineAdapter
mAdapter.setTimelineItems(mTimeline.mTimelineItems);
// notify the adapter the data set has changed
mAdapter.notifyDataSetChanged();
// scroll to the last scroll (updated) position
mLinearLayoutManager.scrollToPositionWithOffset(newScrollToPosition, 0);
}
@Subscribe
public void onTimelineItemChanged(TimelineItemChangeNotification notification) {
int position = notification.position;
Log.d(TAG, "TimelineItemChanged detected for position " + position);
mAdapter.swapTimelineItemAtPosition(mTimeline.mTimelineItems.get(position), position);
//mAdapter.notifyItemRangeChanged(position, position);
Log.d(TAG, "Item for position " + position + " swapped");
}
我在应用首次加载后截取了该应用的屏幕截图。我将快速解释初始化时发生的情况:
这是初始加载后发生的情况的屏幕截图:
时间轴已加载,但某些项目插入了错误的位置。
当滚动离开并返回到之前显示不正确的日期范围时,一切都很好:
最佳答案
关于您的第二种方法。您的代码可能无法正常工作,因为您在 mTimelineItems 和 mCachedEvents 上有Data Race。我看不到您的所有代码,但您似乎在 doInBackground() 中同时使用 mTimelineItems 与 UI 线程,但没有任何同步。
我建议您混合使用第一种和第二种方法:
mTimelineItems) 并将其发送到 AsyncTask。doInBackground() 中异步更改副本并记录所有更改。让我用代码来说明这种方法。
数据管理:
public class AsyncDataUpdater
{
/**
* Example data entity. We will use it
* in our RecyclerView.
*/
public static class TimelineItem
{
public final String name;
public final float value;
public TimelineItem(String name, float value)
{
this.name = name;
this.value = value;
}
}
/**
* That's how we will apply our data changes
* on the RecyclerView.
*/
public static class Diff
{
// 0 - ADD; 1 - CHANGE; 2 - REMOVE;
final int command;
final int position;
Diff(int command, int position)
{
this.command = command;
this.position = position;
}
}
/**
* And that's how we will notify the RecyclerView
* about changes.
*/
public interface DataChangeListener
{
void onDataChanged(ArrayList<Diff> diffs);
}
private static class TaskResult
{
final ArrayList<Diff> diffs;
final ArrayList<TimelineItem> items;
TaskResult(ArrayList<TimelineItem> items, ArrayList<Diff> diffs)
{
this.diffs = diffs;
this.items = items;
}
}
private class InsertEventsTask extends AsyncTask<Void, Void, TaskResult>
{
//NOTE: this is copy of the original data.
private ArrayList<TimelineItem> _old_items;
InsertEventsTask(ArrayList<TimelineItem> items)
{
_old_items = items;
}
@Override
protected TaskResult doInBackground(Void... params)
{
ArrayList<Diff> diffs = new ArrayList<>();
try
{
//TODO: long operation(Database, network, ...).
Thread.sleep(1000);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
//Some crazy manipulation with data...
//NOTE: we change the copy of the original data!
Random rand = new Random();
for(int i = 0; i < 10; i ++)
{
float rnd = rand.nextFloat() * 100.0f;
for(int j = 0; j < _old_items.size(); j++)
{
if(_old_items.get(j).value > rnd)
{
TimelineItem item = new TimelineItem("Item " + rnd, rnd);
//Change data.
_old_items.add(j, item);
//Log the changes.
diffs.add(new Diff(0, j));
break;
}
}
}
for(int i = 0; i < 5; i ++)
{
int rnd_index = rand.nextInt(_old_items.size());
//Change data.
_old_items.remove(rnd_index);
//Log the changes.
diffs.add(new Diff(2, rnd_index));
}
//...
return new TaskResult(_old_items, diffs);
}
@Override
protected void onPostExecute(TaskResult result)
{
super.onPostExecute(result);
//Apply the new data in the UI thread.
_items = result.items;
if(_listener != null)
_listener.onDataChanged(result.diffs);
}
}
private DataChangeListener _listener;
private InsertEventsTask _task = null;
/** Managed data. */
private ArrayList<TimelineItem> _items = new ArrayList<>();
public AsyncDataUpdater()
{
// Some test data.
for(float i = 10.0f; i <= 100.0f; i += 10.0f)
_items.add(new TimelineItem("Item " + i, i));
}
public void setDataChangeListener(DataChangeListener listener)
{
_listener = listener;
}
public void updateDataAsync()
{
if(_task != null)
_task.cancel(true);
// NOTE: we should to make the new copy of the _items array.
_task = new InsertEventsTask(new ArrayList<>(_items));
_task.execute();
}
public int getItemsCount()
{
return _items.size();
}
public TimelineItem getItem(int index)
{
return _items.get(index);
}
}
在 UI 中使用:
public class MainActivity extends AppCompatActivity
{
private static class ViewHolder extends RecyclerView.ViewHolder
{
private final TextView name;
private final ProgressBar value;
ViewHolder(View itemView)
{
super(itemView);
name = (TextView)itemView.findViewById(R.id.tv_name);
value = (ProgressBar)itemView.findViewById(R.id.pb_value);
}
void bind(AsyncDataUpdater.TimelineItem item)
{
name.setText(item.name);
value.setProgress((int)item.value);
}
}
private static class Adapter extends RecyclerView.Adapter<ViewHolder>
implements AsyncDataUpdater.DataChangeListener
{
private final AsyncDataUpdater _data;
Adapter(AsyncDataUpdater data)
{
_data = data;
_data.setDataChangeListener(this);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position)
{
holder.bind(_data.getItem(position));
}
@Override
public int getItemCount()
{
return _data.getItemsCount();
}
@Override
public void onDataChanged(ArrayList<AsyncDataUpdater.Diff> diffs)
{
//Apply changes.
for(AsyncDataUpdater.Diff d : diffs)
{
if(d.command == 0)
notifyItemInserted(d.position);
else if(d.command == 1)
notifyItemChanged(d.position);
else if(d.command == 2)
notifyItemRemoved(d.position);
}
}
}
private AsyncDataUpdater _data = new AsyncDataUpdater();
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView rv_content = (RecyclerView)findViewById(R.id.rv_content);
rv_content.setLayoutManager(new LinearLayoutManager(this));
rv_content.setAdapter(new Adapter(_data));
Button btn_add = (Button)findViewById(R.id.btn_add);
btn_add.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
_data.updateDataAsync();
}
});
}
}
我把Example GH 上的应用程序,因此您可以根据需要进行测试。
更新 1
关于数据竞赛。
this.mTimelineItems = timelineItems; 在 TimelineAdapter() 构造函数中复制了对 ArrayList 的引用,但不是ArrayList 本身的副本。因此,您有两个引用:TimelineAdapter.mTimelineItems 和 Timeline.mTimelineItems,它们都引用相同的 ArrayList 对象。请看this .
当从 Worker Thread 调用 doInBackground() 和从 UI Thread 调用 onProgressUpdate() 时发生数据竞争同时。主要原因是 publishProgress() 没有同步调用onProgressUpdate()。相反,publishProgress()计划在将来UI 线程 上调用onProgressUpdate()。 Here很好地描述了问题。
题外话。
这个:
mTimelineItems.set(position, item);
应该比这更快:
mTimelineItems.remove(position);
mTimelineItems.add(position, item);
关于android - 使用什么设计理念来异步更新UI,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34840470/
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
我正在使用i18n从头开始构建一个多语言网络应用程序,虽然我自己可以处理一大堆yml文件,但我说的语言(非常)有限,最终我想寻求外部帮助帮助。我想知道这里是否有人在使用UI插件/gem(与django上的django-rosetta不同)来处理多个翻译器,其中一些翻译器不愿意或无法处理存储库中的100多个文件,处理语言数据。谢谢&问候,安德拉斯(如果您已经在rubyonrails-talk上遇到了这个问题,我们深表歉意) 最佳答案 有一个rails3branchofthetolkgem在github上。您可以通过在Gemfi
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co