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