处理recyclerview中的倒计时器 - android

时间:2016-07-07 09:09:00

标签: android android-recyclerview countdowntimer android-viewholder

我目前正在开发一款应用程序,该应用程序在其核心功能中处理循环器视图中的多个倒计时器。

现在,我实现这一点的方法是简单地为每个创建的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);
            }
        }
    };
}

我做错了什么,但我似乎无法确定问题所在。

2 个答案:

答案 0 :(得分:1)

无法从倒数计时器中取消倒数计时器。这显然是因为当您调用cancel时已经安排了下一个事件,并且当该事件被触发时,它将触发下一个事件。您的问题可能是因为计时器没有被取消。

我之前必须这样做,我的方法是为所有视图使用一个倒数计时器。你不想调用notifyDataSetChanged,因为这会刷新一切。而是创建一个自定义count up timer,它将WeakReferences数组保存到需要勾选时间的所有可见视图。您可以在每个刻度处一次性更新所有这些视图。您可以将结束时间或开始时间放在View的标记中,因此在您的计数中,imer会从标记中获取结束时间并进行计算。

您可以在生命周期方法中启动和停止计时器。希望这会有所帮助。

答案 1 :(得分:-1)

对于您的问题,建议您使用HandleRunnable。使用它们,您可以获取结束时间事件并执行所需的操作。您可以在https://stackoverflow.com/a/53543180/6711554处查看我的答案。