1/*
2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package androidx.recyclerview.widget;
17
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertNotNull;
21import static org.junit.Assert.assertNotSame;
22import static org.junit.Assert.assertTrue;
23
24import android.content.Context;
25import android.graphics.Canvas;
26import android.util.AttributeSet;
27import android.util.Log;
28import android.view.View;
29
30import java.util.ArrayList;
31import java.util.HashSet;
32import java.util.List;
33import java.util.Set;
34import java.util.concurrent.CountDownLatch;
35import java.util.concurrent.TimeUnit;
36
37/**
38 * Base class for animation related tests.
39 */
40public class BaseRecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest {
41
42    protected static final boolean DEBUG = false;
43
44    protected static final String TAG = "RecyclerViewAnimationsTest";
45
46    AnimationLayoutManager mLayoutManager;
47
48    TestAdapter mTestAdapter;
49
50    public BaseRecyclerViewAnimationsTest() {
51        super(DEBUG);
52    }
53
54    RecyclerView setupBasic(int itemCount) throws Throwable {
55        return setupBasic(itemCount, 0, itemCount);
56    }
57
58    RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount)
59            throws Throwable {
60        return setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null);
61    }
62
63    RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount,
64            TestAdapter testAdapter)
65            throws Throwable {
66        final TestRecyclerView recyclerView = new TestRecyclerView(getActivity());
67        recyclerView.setHasFixedSize(true);
68        if (testAdapter == null) {
69            mTestAdapter = new TestAdapter(itemCount);
70        } else {
71            mTestAdapter = testAdapter;
72        }
73        recyclerView.setAdapter(mTestAdapter);
74        recyclerView.setItemAnimator(createItemAnimator());
75        mLayoutManager = new AnimationLayoutManager();
76        recyclerView.setLayoutManager(mLayoutManager);
77        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = firstLayoutStartIndex;
78        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = firstLayoutItemCount;
79
80        mLayoutManager.expectLayouts(1);
81        recyclerView.expectDraw(1);
82        setRecyclerView(recyclerView);
83        mLayoutManager.waitForLayout(2);
84        recyclerView.waitForDraw(1);
85        mLayoutManager.mOnLayoutCallbacks.reset();
86        getInstrumentation().waitForIdleSync();
87        checkForMainThreadException();
88        assertEquals("extra layouts should not happen", 1, mLayoutManager.getTotalLayoutCount());
89        assertEquals("all expected children should be laid out", firstLayoutItemCount,
90                mLayoutManager.getChildCount());
91        return recyclerView;
92    }
93
94    protected RecyclerView.ItemAnimator createItemAnimator() {
95        return new DefaultItemAnimator();
96    }
97
98    public TestRecyclerView getTestRecyclerView() {
99        return (TestRecyclerView) mRecyclerView;
100    }
101
102    class AnimationLayoutManager extends TestLayoutManager {
103
104        protected int mTotalLayoutCount = 0;
105        private String log;
106
107        OnLayoutCallbacks mOnLayoutCallbacks = new OnLayoutCallbacks() {
108        };
109
110
111
112        @Override
113        public boolean supportsPredictiveItemAnimations() {
114            return true;
115        }
116
117        public String getLog() {
118            return log;
119        }
120
121        private String prepareLog(RecyclerView.Recycler recycler, RecyclerView.State state, boolean done) {
122            StringBuilder builder = new StringBuilder();
123            builder.append("is pre layout:").append(state.isPreLayout()).append(", done:").append(done);
124            builder.append("\nViewHolders:\n");
125            for (RecyclerView.ViewHolder vh : ((TestRecyclerView)mRecyclerView).collectViewHolders()) {
126                builder.append(vh).append("\n");
127            }
128            builder.append("scrap:\n");
129            for (RecyclerView.ViewHolder vh : recycler.getScrapList()) {
130                builder.append(vh).append("\n");
131            }
132
133            if (state.isPreLayout() && !done) {
134                log = "\n" + builder.toString();
135            } else {
136                log += "\n" + builder.toString();
137            }
138            return log;
139        }
140
141        @Override
142        public void expectLayouts(int count) {
143            super.expectLayouts(count);
144            mOnLayoutCallbacks.mLayoutCount = 0;
145        }
146
147        public void setOnLayoutCallbacks(OnLayoutCallbacks onLayoutCallbacks) {
148            mOnLayoutCallbacks = onLayoutCallbacks;
149        }
150
151        @Override
152        public final void onLayoutChildren(RecyclerView.Recycler recycler,
153                RecyclerView.State state) {
154            try {
155                mTotalLayoutCount++;
156                prepareLog(recycler, state, false);
157                if (state.isPreLayout()) {
158                    validateOldPositions(recycler, state);
159                } else {
160                    validateClearedOldPositions(recycler, state);
161                }
162                mOnLayoutCallbacks.onLayoutChildren(recycler, this, state);
163                prepareLog(recycler, state, true);
164            } finally {
165                layoutLatch.countDown();
166            }
167        }
168
169        private void validateClearedOldPositions(RecyclerView.Recycler recycler,
170                RecyclerView.State state) {
171            if (getTestRecyclerView() == null) {
172                return;
173            }
174            for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) {
175                assertEquals("there should NOT be an old position in post layout",
176                        RecyclerView.NO_POSITION, viewHolder.mOldPosition);
177                assertEquals("there should NOT be a pre layout position in post layout",
178                        RecyclerView.NO_POSITION, viewHolder.mPreLayoutPosition);
179            }
180        }
181
182        private void validateOldPositions(RecyclerView.Recycler recycler,
183                RecyclerView.State state) {
184            if (getTestRecyclerView() == null) {
185                return;
186            }
187            for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) {
188                if (!viewHolder.isRemoved() && !viewHolder.isInvalid()) {
189                    assertTrue("there should be an old position in pre-layout",
190                            viewHolder.mOldPosition != RecyclerView.NO_POSITION);
191                }
192            }
193        }
194
195        public int getTotalLayoutCount() {
196            return mTotalLayoutCount;
197        }
198
199        @Override
200        public boolean canScrollVertically() {
201            return true;
202        }
203
204        @Override
205        public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
206                RecyclerView.State state) {
207            mOnLayoutCallbacks.onScroll(dy, recycler, state);
208            return super.scrollVerticallyBy(dy, recycler, state);
209        }
210
211        public void onPostDispatchLayout() {
212            mOnLayoutCallbacks.postDispatchLayout();
213        }
214    }
215
216    abstract class OnLayoutCallbacks {
217
218        int mLayoutMin = Integer.MIN_VALUE;
219
220        int mLayoutItemCount = Integer.MAX_VALUE;
221
222        int expectedPreLayoutItemCount = -1;
223
224        int expectedPostLayoutItemCount = -1;
225
226        int mDeletedViewCount;
227
228        int mLayoutCount = 0;
229
230        void setExpectedItemCounts(int preLayout, int postLayout) {
231            expectedPreLayoutItemCount = preLayout;
232            expectedPostLayoutItemCount = postLayout;
233        }
234
235        void reset() {
236            mLayoutMin = Integer.MIN_VALUE;
237            mLayoutItemCount = Integer.MAX_VALUE;
238            expectedPreLayoutItemCount = -1;
239            expectedPostLayoutItemCount = -1;
240            mLayoutCount = 0;
241        }
242
243        void beforePreLayout(RecyclerView.Recycler recycler,
244                AnimationLayoutManager lm, RecyclerView.State state) {
245            mDeletedViewCount = 0;
246            for (int i = 0; i < lm.getChildCount(); i++) {
247                View v = lm.getChildAt(i);
248                if (lm.getLp(v).isItemRemoved()) {
249                    mDeletedViewCount++;
250                }
251            }
252        }
253
254        void doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
255                RecyclerView.State state) {
256            if (DEBUG) {
257                Log.d(TAG, "item count " + state.getItemCount());
258            }
259            lm.detachAndScrapAttachedViews(recycler);
260            final int start = mLayoutMin == Integer.MIN_VALUE ? 0 : mLayoutMin;
261            final int count = mLayoutItemCount
262                    == Integer.MAX_VALUE ? state.getItemCount() : mLayoutItemCount;
263            lm.layoutRange(recycler, start, start + count);
264            assertEquals("correct # of children should be laid out",
265                    count, lm.getChildCount());
266            lm.assertVisibleItemPositions();
267        }
268
269        private void assertNoPreLayoutPosition(RecyclerView.Recycler recycler) {
270            for (RecyclerView.ViewHolder vh : recycler.mAttachedScrap) {
271                assertPreLayoutPosition(vh);
272            }
273        }
274
275        private void assertNoPreLayoutPosition(RecyclerView.LayoutManager lm) {
276            for (int i = 0; i < lm.getChildCount(); i ++) {
277                final RecyclerView.ViewHolder vh = mRecyclerView
278                        .getChildViewHolder(lm.getChildAt(i));
279                assertPreLayoutPosition(vh);
280            }
281        }
282
283        private void assertPreLayoutPosition(RecyclerView.ViewHolder vh) {
284            assertEquals("in post layout, there should not be a view holder w/ a pre "
285                    + "layout position", RecyclerView.NO_POSITION, vh.mPreLayoutPosition);
286            assertEquals("in post layout, there should not be a view holder w/ an old "
287                    + "layout position", RecyclerView.NO_POSITION, vh.mOldPosition);
288        }
289
290        void onLayoutChildren(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
291                RecyclerView.State state) {
292            if (state.isPreLayout()) {
293                if (expectedPreLayoutItemCount != -1) {
294                    assertEquals("on pre layout, state should return abstracted adapter size",
295                            expectedPreLayoutItemCount, state.getItemCount());
296                }
297                beforePreLayout(recycler, lm, state);
298            } else {
299                if (expectedPostLayoutItemCount != -1) {
300                    assertEquals("on post layout, state should return real adapter size",
301                            expectedPostLayoutItemCount, state.getItemCount());
302                }
303                beforePostLayout(recycler, lm, state);
304            }
305            if (!state.isPreLayout()) {
306                assertNoPreLayoutPosition(recycler);
307            }
308            doLayout(recycler, lm, state);
309            if (state.isPreLayout()) {
310                afterPreLayout(recycler, lm, state);
311            } else {
312                afterPostLayout(recycler, lm, state);
313                assertNoPreLayoutPosition(lm);
314            }
315            mLayoutCount++;
316        }
317
318        void afterPreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
319                RecyclerView.State state) {
320        }
321
322        void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
323                RecyclerView.State state) {
324        }
325
326        void afterPostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
327                RecyclerView.State state) {
328        }
329
330        void postDispatchLayout() {
331        }
332
333        public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
334
335        }
336    }
337
338    class TestRecyclerView extends RecyclerView {
339
340        CountDownLatch drawLatch;
341
342        public TestRecyclerView(Context context) {
343            super(context);
344        }
345
346        public TestRecyclerView(Context context, AttributeSet attrs) {
347            super(context, attrs);
348        }
349
350        public TestRecyclerView(Context context, AttributeSet attrs, int defStyle) {
351            super(context, attrs, defStyle);
352        }
353
354        @Override
355        void initAdapterManager() {
356            super.initAdapterManager();
357            mAdapterHelper.mOnItemProcessedCallback = new Runnable() {
358                @Override
359                public void run() {
360                    validatePostUpdateOp();
361                }
362            };
363        }
364
365        @Override
366        boolean isAccessibilityEnabled() {
367            return true;
368        }
369
370        public void expectDraw(int count) {
371            drawLatch = new CountDownLatch(count);
372        }
373
374        public void waitForDraw(long timeout) throws Throwable {
375            drawLatch.await(timeout * (DEBUG ? 100 : 1), TimeUnit.SECONDS);
376            assertEquals("all expected draws should happen at the expected time frame",
377                    0, drawLatch.getCount());
378        }
379
380        List<ViewHolder> collectViewHolders() {
381            List<ViewHolder> holders = new ArrayList<ViewHolder>();
382            final int childCount = getChildCount();
383            for (int i = 0; i < childCount; i++) {
384                ViewHolder holder = getChildViewHolderInt(getChildAt(i));
385                if (holder != null) {
386                    holders.add(holder);
387                }
388            }
389            return holders;
390        }
391
392
393        private void validateViewHolderPositions() {
394            final Set<Integer> existingOffsets = new HashSet<Integer>();
395            int childCount = getChildCount();
396            StringBuilder log = new StringBuilder();
397            for (int i = 0; i < childCount; i++) {
398                ViewHolder vh = getChildViewHolderInt(getChildAt(i));
399                TestViewHolder tvh = (TestViewHolder) vh;
400                log.append(tvh.mBoundItem).append(vh)
401                        .append(" hidden:")
402                        .append(mChildHelper.mHiddenViews.contains(vh.itemView))
403                        .append("\n");
404            }
405            for (int i = 0; i < childCount; i++) {
406                ViewHolder vh = getChildViewHolderInt(getChildAt(i));
407                if (vh.isInvalid()) {
408                    continue;
409                }
410                if (vh.getLayoutPosition() < 0) {
411                    LayoutManager lm = getLayoutManager();
412                    for (int j = 0; j < lm.getChildCount(); j ++) {
413                        assertNotSame("removed view holder should not be in LM's child list",
414                                vh.itemView, lm.getChildAt(j));
415                    }
416                } else if (!mChildHelper.mHiddenViews.contains(vh.itemView)) {
417                    if (!existingOffsets.add(vh.getLayoutPosition())) {
418                        throw new IllegalStateException("view holder position conflict for "
419                                + "existing views " + vh + "\n" + log);
420                    }
421                }
422            }
423        }
424
425        void validatePostUpdateOp() {
426            try {
427                validateViewHolderPositions();
428                if (super.mState.isPreLayout()) {
429                    validatePreLayoutSequence((AnimationLayoutManager) getLayoutManager());
430                }
431                validateAdapterPosition((AnimationLayoutManager) getLayoutManager());
432            } catch (Throwable t) {
433                postExceptionToInstrumentation(t);
434            }
435        }
436
437
438
439        private void validateAdapterPosition(AnimationLayoutManager lm) {
440            for (ViewHolder vh : collectViewHolders()) {
441                if (!vh.isRemoved() && vh.mPreLayoutPosition >= 0) {
442                    assertEquals("adapter position calculations should match view holder "
443                                    + "pre layout:" + mState.isPreLayout()
444                                    + " positions\n" + vh + "\n" + lm.getLog(),
445                            mAdapterHelper.findPositionOffset(vh.mPreLayoutPosition), vh.mPosition);
446                }
447            }
448        }
449
450        // ensures pre layout positions are continuous block. This is not necessarily a case
451        // but valid in test RV
452        private void validatePreLayoutSequence(AnimationLayoutManager lm) {
453            Set<Integer> preLayoutPositions = new HashSet<Integer>();
454            for (ViewHolder vh : collectViewHolders()) {
455                assertTrue("pre layout positions should be distinct " + lm.getLog(),
456                        preLayoutPositions.add(vh.mPreLayoutPosition));
457            }
458            int minPos = Integer.MAX_VALUE;
459            for (Integer pos : preLayoutPositions) {
460                if (pos < minPos) {
461                    minPos = pos;
462                }
463            }
464            for (int i = 1; i < preLayoutPositions.size(); i++) {
465                assertNotNull("next position should exist " + lm.getLog(),
466                        preLayoutPositions.contains(minPos + i));
467            }
468        }
469
470        @Override
471        protected void dispatchDraw(Canvas canvas) {
472            super.dispatchDraw(canvas);
473            if (drawLatch != null) {
474                drawLatch.countDown();
475            }
476        }
477
478        @Override
479        void dispatchLayout() {
480            try {
481                super.dispatchLayout();
482                if (getLayoutManager() instanceof AnimationLayoutManager) {
483                    ((AnimationLayoutManager) getLayoutManager()).onPostDispatchLayout();
484                }
485            } catch (Throwable t) {
486                postExceptionToInstrumentation(t);
487            }
488
489        }
490
491
492    }
493
494    abstract class AdapterOps {
495
496        final public void run(TestAdapter adapter) throws Throwable {
497            onRun(adapter);
498        }
499
500        abstract void onRun(TestAdapter testAdapter) throws Throwable;
501    }
502
503    static class CollectPositionResult {
504
505        // true if found in scrap
506        public RecyclerView.ViewHolder scrapResult;
507
508        public RecyclerView.ViewHolder adapterResult;
509
510        static CollectPositionResult fromScrap(RecyclerView.ViewHolder viewHolder) {
511            CollectPositionResult cpr = new CollectPositionResult();
512            cpr.scrapResult = viewHolder;
513            return cpr;
514        }
515
516        static CollectPositionResult fromAdapter(RecyclerView.ViewHolder viewHolder) {
517            CollectPositionResult cpr = new CollectPositionResult();
518            cpr.adapterResult = viewHolder;
519            return cpr;
520        }
521
522        @Override
523        public String toString() {
524            return "CollectPositionResult{" +
525                    "scrapResult=" + scrapResult +
526                    ", adapterResult=" + adapterResult +
527                    '}';
528        }
529    }
530
531    static class PositionConstraint {
532
533        public static enum Type {
534            scrap,
535            adapter,
536            adapterScrap /*first pass adapter, second pass scrap*/
537        }
538
539        Type mType;
540
541        int mOldPos; // if VH
542
543        int mPreLayoutPos;
544
545        int mPostLayoutPos;
546
547        int mValidateCount = 0;
548
549        public static PositionConstraint scrap(int oldPos, int preLayoutPos, int postLayoutPos) {
550            PositionConstraint constraint = new PositionConstraint();
551            constraint.mType = Type.scrap;
552            constraint.mOldPos = oldPos;
553            constraint.mPreLayoutPos = preLayoutPos;
554            constraint.mPostLayoutPos = postLayoutPos;
555            return constraint;
556        }
557
558        public static PositionConstraint adapterScrap(int preLayoutPos, int position) {
559            PositionConstraint constraint = new PositionConstraint();
560            constraint.mType = Type.adapterScrap;
561            constraint.mOldPos = RecyclerView.NO_POSITION;
562            constraint.mPreLayoutPos = preLayoutPos;
563            constraint.mPostLayoutPos = position;// adapter pos does not change
564            return constraint;
565        }
566
567        public static PositionConstraint adapter(int position) {
568            PositionConstraint constraint = new PositionConstraint();
569            constraint.mType = Type.adapter;
570            constraint.mPreLayoutPos = RecyclerView.NO_POSITION;
571            constraint.mOldPos = RecyclerView.NO_POSITION;
572            constraint.mPostLayoutPos = position;// adapter pos does not change
573            return constraint;
574        }
575
576        public void assertValidate() {
577            int expectedValidate = 0;
578            if (mPreLayoutPos >= 0) {
579                expectedValidate ++;
580            }
581            if (mPostLayoutPos >= 0) {
582                expectedValidate ++;
583            }
584            assertEquals("should run all validates", expectedValidate, mValidateCount);
585        }
586
587        @Override
588        public String toString() {
589            return "Cons{" +
590                    "t=" + mType.name() +
591                    ", old=" + mOldPos +
592                    ", pre=" + mPreLayoutPos +
593                    ", post=" + mPostLayoutPos +
594                    '}';
595        }
596
597        public void validate(RecyclerView.State state, CollectPositionResult result, String log) {
598            mValidateCount ++;
599            assertNotNull(this + ": result should not be null\n" + log, result);
600            RecyclerView.ViewHolder viewHolder;
601            if (mType == Type.scrap || (mType == Type.adapterScrap && !state.isPreLayout())) {
602                assertNotNull(this + ": result should come from scrap\n" + log, result.scrapResult);
603                viewHolder = result.scrapResult;
604            } else {
605                assertNotNull(this + ": result should come from adapter\n"  + log,
606                        result.adapterResult);
607                assertEquals(this + ": old position should be none when it came from adapter\n" + log,
608                        RecyclerView.NO_POSITION, result.adapterResult.getOldPosition());
609                viewHolder = result.adapterResult;
610            }
611            if (state.isPreLayout()) {
612                assertEquals(this + ": pre-layout position should match\n" + log, mPreLayoutPos,
613                        viewHolder.mPreLayoutPosition == -1 ? viewHolder.mPosition :
614                                viewHolder.mPreLayoutPosition);
615                assertEquals(this + ": pre-layout getPosition should match\n" + log, mPreLayoutPos,
616                        viewHolder.getLayoutPosition());
617                if (mType == Type.scrap) {
618                    assertEquals(this + ": old position should match\n" + log, mOldPos,
619                            result.scrapResult.getOldPosition());
620                }
621            } else if (mType == Type.adapter || mType == Type.adapterScrap || !result.scrapResult
622                    .isRemoved()) {
623                assertEquals(this + ": post-layout position should match\n" + log + "\n\n"
624                        + viewHolder, mPostLayoutPos, viewHolder.getLayoutPosition());
625            }
626        }
627    }
628
629    static class LoggingInfo extends RecyclerView.ItemAnimator.ItemHolderInfo {
630        final RecyclerView.ViewHolder viewHolder;
631        @RecyclerView.ItemAnimator.AdapterChanges
632        final int changeFlags;
633        final List<Object> payloads;
634
635        LoggingInfo(RecyclerView.ViewHolder viewHolder, int changeFlags, List<Object> payloads) {
636            this.viewHolder = viewHolder;
637            this.changeFlags = changeFlags;
638            if (payloads != null) {
639                this.payloads = new ArrayList<>();
640                this.payloads.addAll(payloads);
641            } else {
642                this.payloads = null;
643            }
644            setFrom(viewHolder);
645        }
646
647        @Override
648        public String toString() {
649            return "LoggingInfo{" +
650                    "changeFlags=" + changeFlags +
651                    ", payloads=" + payloads +
652                    '}';
653        }
654    }
655
656    static class AnimateChange extends AnimateLogBase {
657
658        final RecyclerView.ViewHolder newHolder;
659
660        public AnimateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
661                LoggingInfo pre, LoggingInfo post) {
662            super(oldHolder, pre, post);
663            this.newHolder = newHolder;
664        }
665    }
666
667    static class AnimatePersistence extends AnimateLogBase {
668
669        public AnimatePersistence(RecyclerView.ViewHolder viewHolder, LoggingInfo pre,
670                LoggingInfo post) {
671            super(viewHolder, pre, post);
672        }
673    }
674
675    static class AnimateAppearance extends AnimateLogBase {
676        public AnimateAppearance(RecyclerView.ViewHolder viewHolder, LoggingInfo pre,
677                LoggingInfo post) {
678            super(viewHolder, pre, post);
679        }
680    }
681
682    static class AnimateDisappearance extends AnimateLogBase {
683        public AnimateDisappearance(RecyclerView.ViewHolder viewHolder, LoggingInfo pre,
684                LoggingInfo post) {
685            super(viewHolder, pre, post);
686        }
687    }
688    static class AnimateLogBase {
689
690        public final RecyclerView.ViewHolder viewHolder;
691        public final LoggingInfo preInfo;
692        public final LoggingInfo postInfo;
693
694        public AnimateLogBase(RecyclerView.ViewHolder viewHolder, LoggingInfo pre,
695                LoggingInfo postInfo) {
696            this.viewHolder = viewHolder;
697            this.preInfo = pre;
698            this.postInfo = postInfo;
699        }
700
701        public String log() {
702            return getClass().getSimpleName() + "[" +  log(preInfo) + " - " + log(postInfo) + "]";
703        }
704
705        public String log(LoggingInfo info) {
706            return info == null ? "null" : info.toString();
707        }
708
709        @Override
710        public boolean equals(Object o) {
711            if (this == o) {
712                return true;
713            }
714            if (o == null || getClass() != o.getClass()) {
715                return false;
716            }
717
718            AnimateLogBase that = (AnimateLogBase) o;
719
720            if (viewHolder != null ? !viewHolder.equals(that.viewHolder)
721                    : that.viewHolder != null) {
722                return false;
723            }
724            if (preInfo != null ? !preInfo.equals(that.preInfo) : that.preInfo != null) {
725                return false;
726            }
727            return !(postInfo != null ? !postInfo.equals(that.postInfo) : that.postInfo != null);
728
729        }
730
731        @Override
732        public int hashCode() {
733            int result = viewHolder != null ? viewHolder.hashCode() : 0;
734            result = 31 * result + (preInfo != null ? preInfo.hashCode() : 0);
735            result = 31 * result + (postInfo != null ? postInfo.hashCode() : 0);
736            return result;
737        }
738    }
739}
740