我目前正在开发一款应用程序,该应用程序在其核心功能中处理循环器视图中的多个倒计时器。
现在,我实现这一点的方法是简单地为每个创建的ViewHolder启动倒计时器。我的问题有时候只要一个计时器完成,所有其他计时器都表现得很奇怪。
例如,我希望每次计时器启动时,该计时器的ViewHolder将更改上述类所引用的CardView UI组件的背景。
实际发生的情况是,如果一个计时器启动,则访问并更改其他视图持有者。
这是我的适配器代码,所有这些都发生了。我试图尽可能多地记录:
package bikurim.silverfix.com.bikurim.adapters;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.os.CountDownTimer;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import bikurim.silverfix.com.bikurim.Constants;
import bikurim.silverfix.com.bikurim.models.Family;
import bikurim.silverfix.com.bikurim.R;
import bikurim.silverfix.com.bikurim.utils.CountDownManager;
import bikurim.silverfix.com.bikurim.utils.TimerEventListener;
public class FamiliesAdapter extends RecyclerView.Adapter<FamiliesAdapter.FamilyViewHolder> implements Filterable, TimerEventListener {
// last position holds the last position of the element that was added, for animation purposes
private static int lastPosition = -1;
private Context context;
private ArrayList<Family> families;
private ArrayList<Family> dataSet;
private FamilyFilter filter;
private CountDownManager countDownManager;
public FamiliesAdapter(Context context, ArrayList<Family> families) {
this.context = context;
this.dataSet = families;
this.families = dataSet;
countDownManager = new CountDownManager(Constants.Values.TIME_INTERVAL, this);
countDownManager.start();
}
@Override
public FamilyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// Inflating the view from a given layout resource file
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.family_list_item, parent, false);
final FamilyViewHolder pvh = new FamilyViewHolder(v);
return pvh;
}
@Override
public void onBindViewHolder(final FamilyViewHolder holder, int position) {
// Binds the Family object to the holder view
Family family = families.get(position);
holder.bindData(family);
countDownManager.addTimer(holder);
// Sets animation on the given view, in case it wasn't displayed before
setSlideAnimation(holder.cardView, position, false);
}
@Override
public int getItemCount() {
return families != null ? families.size() : 0;
}
/* Gets the current the filter object */
@Override
public Filter getFilter() {
if (filter == null)
filter = new FamilyFilter();
return filter;
}
/* The following 2 methods are an implementations of the ViewHolderListener.
* Inside every view holder lies an instance of ViewHolderListener, for communication between the two*/
public void startAnimationOnItem(FamilyViewHolder holder, boolean isEndAnimation) {
setSlideAnimation(holder.cardView, holder.getAdapterPosition(), isEndAnimation);
}
/* Changes the UI of a holder to a time's up view with a flickering ImageButton and a TextView*/
@Override
public void onFinish(FamilyViewHolder holder) {
// Switches between the clock icon to the alarm icon
holder.clock.setVisibility(View.GONE);
holder.remove.setVisibility(View.VISIBLE);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
params.addRule(RelativeLayout.RIGHT_OF, holder.remove.getId());
params.addRule(RelativeLayout.CENTER_VERTICAL);
holder.timeLeft.setLayoutParams(params);
holder.timeLeft.setText(context.getString(R.string.time_up_message));
setFadeAnimation(holder.remove);
holder.cardView.setBackgroundResource(R.color.cardview_light_background);
startAnimationOnItem(holder, true);
holder.family.timeLeft = 0;
}
@Override
public void onLessThanMinute(FamilyViewHolder holder) {
holder.cardView.setBackgroundResource(R.color.time_up_bg);
holder.isBackgroundChanged = true;
}
/* Starts a slide in animation for a given Card View */
private void setSlideAnimation(View viewToAnimate, int position, boolean isEndAnimation) {
Animation animation = AnimationUtils.loadAnimation(context, android.R.anim.slide_in_left);
if (!isEndAnimation) {
if (position > lastPosition) {
viewToAnimate.startAnimation(animation);
lastPosition = position;
return;
}
}
viewToAnimate.startAnimation(animation);
}
private void setFadeAnimation(View viewToAnimate) {
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(viewToAnimate, "alpha", 1f, .1f);
fadeOut.setDuration(750);
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(viewToAnimate, "alpha", .1f, 1f);
fadeIn.setDuration(750);
final AnimatorSet mAnimationSet = new AnimatorSet();
mAnimationSet.play(fadeIn).after(fadeOut);
mAnimationSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mAnimationSet.start();
}
});
mAnimationSet.start();
}
public void removeData(int pos, FamilyViewHolder holder) {
// Sets the last position to the given deleted position for animation purposes
lastPosition = pos;
// Removes the family object from the data set
families.remove(pos);
notifyItemRemoved(pos);
notifyItemRangeChanged(pos, getItemCount());
// Cancels the the timer and removes it from the entry set
countDownManager.cancelTimer(holder);
}
/* Cancels the timers and clears the entry set */
public void cancelTimers() {
countDownManager.reset();
countDownManager.stop();
}
/* Clears the adapter's data and resets the last position to -1 */
public void clearData() {
cancelTimers();
filter = null;
lastPosition = -1;
}
/* The official view holder of the adapter. Holds references to the relevant views inside the layout, and */
public static class FamilyViewHolder extends RecyclerView.ViewHolder {
public boolean isBackgroundChanged;
public CardView cardView;
public TextView personLname;
public TextView timeLeft;
public TextView visitors;
public ImageView clock;
public ImageButton remove;
public Family family;
private ColorStateList colorStateList;
public FamilyViewHolder(View itemView) {
super(itemView);
// isBackgroundChanged represents whether the holder is under 60 seconds or not
isBackgroundChanged = false;
// Getting the references for the UI components
cardView = (CardView) itemView.findViewById(R.id.cv);
personLname = (TextView) itemView.findViewById(R.id.person_Lname);
visitors = (TextView) itemView.findViewById(R.id.visitors);
timeLeft = (TextView) itemView.findViewById(R.id.person_timeLeft);
clock = (ImageView) itemView.findViewById(R.id.clock);
remove = (ImageButton) itemView.findViewById(R.id.time_up);
// Sets a reference to the old colors of the text view
colorStateList = timeLeft.getTextColors();
}
public void bindData(Family item) {
family = item;
personLname.setText(family.lastName);
visitors.setText("מבקרים: " + family.visitorsNum);
}
public ColorStateList getOriginalColors() {
return colorStateList;
}
}
/* Filter class that queries the constraint on the data set every whenInMillis the user types a key */
private class FamilyFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
if (constraint.length() == 0 || constraint == null) {
results.values = dataSet;
results.count = dataSet.size();
} else {
ArrayList<Family> queryResults = new ArrayList<Family>();
for (Family f : dataSet) {
if (constraint.charAt(0) == f.lastName.toUpperCase().indexOf(0)) {
queryResults.add(f);
} else if (f.lastName.toUpperCase().contains(constraint.toString().toUpperCase())) {
queryResults.add(f);
}
}
results.values = queryResults;
results.count = queryResults.size();
}
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
// Now we have to inform the adapter about the new list filtered
synchronized (families) {
families = (ArrayList<Family>) results.values;
}
notifyDataSetChanged();
}
}
}
当计时器结束时,为什么视图持有者会被扰乱?
更新:我尝试实现这一点有点不同。而不是使用多个倒计时,我只使用一个持有需要更新的持有者数组。但问题仍然存在。这是我的CountDownManager类:
package bikurim.silverfix.com.bikurim.utils;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import java.util.ArrayList;
import bikurim.silverfix.com.bikurim.Constants;
import bikurim.silverfix.com.bikurim.adapters.FamiliesAdapter;
/**
* Created by David on 07/07/2016.
* @author David
* Inspired by MiguelLavigne
*
* A custom CountDownTimer class, which takes care of all the running timers
*/
public class CountDownManager {
private final long interval;
private long base;
// Holds references for all the visible text views
private ArrayList<FamiliesAdapter.FamilyViewHolder> holders;
private TimerEventListener listener;
public CountDownManager(long interval, TimerEventListener listener) {
this.listener = listener;
this.interval = interval;
holders = new ArrayList<>();
}
public void start() {
base = System.currentTimeMillis();
handler.sendMessage(handler.obtainMessage(MSG));
}
public void stop() {
handler.removeMessages(MSG);
}
public void reset() {
synchronized (this) {
base = System.currentTimeMillis();
}
}
public void addTimer(FamiliesAdapter.FamilyViewHolder holder) {
synchronized (holders) {
if(!holders.contains(holder))
holders.add(holder);
}
}
public void cancelTimer(FamiliesAdapter.FamilyViewHolder holder) {
synchronized (holders) {
holders.remove(holder);
}
}
public void onTick(long elapsedTime) {
long timeLeft, lengthOfVisit;
FamiliesAdapter.FamilyViewHolder holderToDelete = null;
for (FamiliesAdapter.FamilyViewHolder holder : holders) {
lengthOfVisit = holder.family.whenInMillis - base;
timeLeft = lengthOfVisit - elapsedTime;
if(timeLeft > 0) {
if(timeLeft <= Constants.Values.ALERT_TIME && holder.isBackgroundChanged != false) {
listener.onLessThanMinute(holder);
}
holder.family.timeLeft = timeLeft;
holder.timeLeft.setText(Utils.updateFormatTime(timeLeft));
} else {
listener.onFinish(holder);
holderToDelete = holder;
}
}
// Checks if there is a holder who's ran out of time. If so, removes it from the list
if(holderToDelete != null)
holders.remove(holderToDelete);
}
private static final int MSG = 1;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
synchronized (CountDownManager.this) {
long elapsedTime = System.currentTimeMillis() - base;
onTick(elapsedTime);
sendMessageDelayed(obtainMessage(MSG), interval);
}
}
};
}
我做错了什么,但我似乎无法确定问题所在。
答案 0 :(得分:1)
无法从倒数计时器中取消倒数计时器。这显然是因为当您调用cancel时已经安排了下一个事件,并且当该事件被触发时,它将触发下一个事件。您的问题可能是因为计时器没有被取消。
我之前必须这样做,我的方法是为所有视图使用一个倒数计时器。你不想调用notifyDataSetChanged,因为这会刷新一切。而是创建一个自定义count up timer,它将WeakReferences数组保存到需要勾选时间的所有可见视图。您可以在每个刻度处一次性更新所有这些视图。您可以将结束时间或开始时间放在View的标记中,因此在您的计数中,imer会从标记中获取结束时间并进行计算。
您可以在生命周期方法中启动和停止计时器。希望这会有所帮助。
答案 1 :(得分:-1)
对于您的问题,建议您使用Handle
和Runnable
。使用它们,您可以获取结束时间事件并执行所需的操作。您可以在https://stackoverflow.com/a/53543180/6711554处查看我的答案。