diff --git a/app/src/main/java/com/hippo/ehviewer/ui/fragment/GalleryListFragment.java b/app/src/main/java/com/hippo/ehviewer/ui/fragment/GalleryListFragment.java index e59395cb4..84681b143 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/fragment/GalleryListFragment.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/fragment/GalleryListFragment.java @@ -15,7 +15,5 @@ package com.hippo.ehviewer.ui.fragment; -import android.support.v4.app.Fragment; - -public class GalleryListFragment extends Fragment{ +public class GalleryListFragment extends SceneFragment { } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/fragment/SceneFragment.java b/app/src/main/java/com/hippo/ehviewer/ui/fragment/SceneFragment.java new file mode 100644 index 000000000..ab2c334d2 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/fragment/SceneFragment.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.ui.fragment; + +import android.support.v4.app.Fragment; + +import com.hippo.scene.Scene; + +public class SceneFragment extends Fragment { + + private Scene mScene; + + public Scene getScene() { + return mScene; + } + + public void setScene(Scene scene) { + mScene = scene; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/fragment/SearchFragment.java b/app/src/main/java/com/hippo/ehviewer/ui/fragment/SearchFragment.java index 1253c2a13..27eaf84dd 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/fragment/SearchFragment.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/fragment/SearchFragment.java @@ -16,9 +16,12 @@ package com.hippo.ehviewer.ui.fragment; import android.app.Activity; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; -import android.support.v4.app.Fragment; +import android.provider.MediaStore; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; @@ -26,8 +29,11 @@ import android.view.ViewGroup; import com.hippo.ehviewer.R; import com.hippo.ehviewer.data.GalleryListUrlBuilder; +import com.hippo.ehviewer.widget.SearchLayout; +import com.hippo.scene.Scene; -public class SearchFragment extends Fragment { +public class SearchFragment extends SceneFragment implements SearchLayout.SearhLayoutHelper, + Scene.ActivityResultListener { private static final String VIEW_STATE_TAG = "android:view_state"; @@ -36,6 +42,7 @@ public class SearchFragment extends Fragment { private OnSearchListener mOnSearchListener; private View mRootView; + private SearchLayout mSearchLayout; public void setOnSearchListener(OnSearchListener listener) { mOnSearchListener = listener; @@ -58,6 +65,9 @@ public class SearchFragment extends Fragment { mActivity = getActivity(); mRootView = inflater.inflate(R.layout.fragment_search, container, false); + mSearchLayout = (SearchLayout) mRootView.findViewById(R.id.search_layout); + + mSearchLayout.setHelper(this); return mRootView; } @@ -69,6 +79,31 @@ public class SearchFragment extends Fragment { mRootView = null; } + @Override + public void requestSelectImage() { + Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + getScene().startActivityForResult(intent, this); + } + + @Override + public void onGetResult(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK && data != null) { + Uri selectedImage = data.getData(); + String[] filePathColumn = { MediaStore.Images.Media.DATA }; + + Cursor cursor = mActivity.getContentResolver().query(selectedImage, filePathColumn, null, null, null); + cursor.moveToFirst(); + + int columnIndex = cursor.getColumnIndex(filePathColumn[0]); + String imagePath = cursor.getString(columnIndex); + cursor.close(); + + if (imagePath != null) { + mSearchLayout.onSelectImage(imagePath); + } + } + } + public interface OnSearchListener { public void onSearch(GalleryListUrlBuilder glub); } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java index de97a281f..4c7470514 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java @@ -93,10 +93,13 @@ public class GalleryListScene extends Scene implements SearchFragment.OnSearchLi public Fragment getItem(int i) { if (i == 0) { SearchFragment fragment = new SearchFragment(); + fragment.setScene(GalleryListScene.this); fragment.setOnSearchListener(GalleryListScene.this); return fragment; } else { - return new GalleryListFragment(); + GalleryListFragment fragment = new GalleryListFragment(); + fragment.setScene(GalleryListScene.this); + return fragment; } } } diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SearchItemAnimator.java b/app/src/main/java/com/hippo/ehviewer/widget/SearchItemAnimator.java new file mode 100644 index 000000000..72eff81b4 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/SearchItemAnimator.java @@ -0,0 +1,664 @@ +/* + * Copyright (C) 2015 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hippo.ehviewer.widget; + +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPropertyAnimatorCompat; +import android.support.v4.view.ViewPropertyAnimatorListener; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.view.View; + +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.ObjectAnimator; +import com.nineoldandroids.animation.ValueAnimator; +import com.nineoldandroids.view.ViewHelper; + +import java.util.ArrayList; +import java.util.List; + +/** + * This implementation of {@link android.support.v7.widget.RecyclerView.ItemAnimator} provides basic + * animations on remove, add, and move events that happen to the items in + * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default. + * + * @see android.support.v7.widget.RecyclerView#setItemAnimator(android.support.v7.widget.RecyclerView.ItemAnimator) + */ +public class SearchItemAnimator extends RecyclerView.ItemAnimator { + private static final boolean DEBUG = false; + + private ArrayList mPendingRemovals = new ArrayList(); + private ArrayList mPendingAdditions = new ArrayList(); + private ArrayList mPendingMoves = new ArrayList(); + private ArrayList mPendingChanges = new ArrayList(); + + private ArrayList> mAdditionsList = + new ArrayList>(); + private ArrayList> mMovesList = new ArrayList>(); + private ArrayList> mChangesList = new ArrayList>(); + + private ArrayList mAddAnimations = new ArrayList(); + private ArrayList mMoveAnimations = new ArrayList(); + private ArrayList mRemoveAnimations = new ArrayList(); + private ArrayList mChangeAnimations = new ArrayList(); + + private RecyclerView mHolder; + + private static class MoveInfo { + public ViewHolder holder; + public int fromX, fromY, toX, toY; + + private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) { + this.holder = holder; + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + } + + private static class ChangeInfo { + public ViewHolder oldHolder, newHolder; + public int fromX, fromY, toX, toY; + private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) { + this.oldHolder = oldHolder; + this.newHolder = newHolder; + } + + private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + this(oldHolder, newHolder); + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + + @Override + public String toString() { + return "ChangeInfo{" + + "oldHolder=" + oldHolder + + ", newHolder=" + newHolder + + ", fromX=" + fromX + + ", fromY=" + fromY + + ", toX=" + toX + + ", toY=" + toY + + '}'; + } + } + + public SearchItemAnimator(RecyclerView holder) { + mHolder = holder; + } + + @Override + public void runPendingAnimations() { + boolean removalsPending = !mPendingRemovals.isEmpty(); + boolean movesPending = !mPendingMoves.isEmpty(); + boolean changesPending = !mPendingChanges.isEmpty(); + boolean additionsPending = !mPendingAdditions.isEmpty(); + if (!removalsPending && !movesPending && !additionsPending && !changesPending) { + // nothing to animate + return; + } + // First, remove stuff + for (ViewHolder holder : mPendingRemovals) { + animateRemoveImpl(holder); + } + mPendingRemovals.clear(); + // Next, move stuff + if (movesPending) { + final ArrayList moves = new ArrayList(); + moves.addAll(mPendingMoves); + mMovesList.add(moves); + mPendingMoves.clear(); + Runnable mover = new Runnable() { + @Override + public void run() { + for (MoveInfo moveInfo : moves) { + animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, + moveInfo.toX, moveInfo.toY); + } + moves.clear(); + mMovesList.remove(moves); + } + }; + if (removalsPending) { + View view = moves.get(0).holder.itemView; + ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); + } else { + mover.run(); + } + } + // Next, change stuff, to run in parallel with move animations + if (changesPending) { + final ArrayList changes = new ArrayList(); + changes.addAll(mPendingChanges); + mChangesList.add(changes); + mPendingChanges.clear(); + Runnable changer = new Runnable() { + @Override + public void run() { + for (ChangeInfo change : changes) { + animateChangeImpl(change); + } + changes.clear(); + mChangesList.remove(changes); + } + }; + if (removalsPending) { + ViewHolder holder = changes.get(0).oldHolder; + ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); + } else { + changer.run(); + } + } + // Next, add stuff + if (additionsPending) { + final ArrayList additions = new ArrayList(); + additions.addAll(mPendingAdditions); + mAdditionsList.add(additions); + mPendingAdditions.clear(); + Runnable adder = new Runnable() { + public void run() { + for (ViewHolder holder : additions) { + animateAddImpl(holder); + } + additions.clear(); + mAdditionsList.remove(additions); + } + }; + if (removalsPending || movesPending || changesPending) { + long removeDuration = removalsPending ? getRemoveDuration() : 0; + long moveDuration = movesPending ? getMoveDuration() : 0; + long changeDuration = changesPending ? getChangeDuration() : 0; + long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); + View view = additions.get(0).itemView; + ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); + } else { + adder.run(); + } + } + } + + @Override + public boolean animateRemove(final ViewHolder holder) { + endAnimation(holder); + mPendingRemovals.add(holder); + return true; + } + + private void animateRemoveImpl(final ViewHolder holder) { + final View view = holder.itemView; + mRemoveAnimations.add(holder); + final ValueAnimator objAnimator = ObjectAnimator.ofFloat(view, "translationX", view.getWidth()); + objAnimator.setDuration(getRemoveDuration()); + objAnimator.addListener( + new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + dispatchRemoveStarting(holder); + } + + @Override + public void onAnimationEnd(Animator animation) { + animation.removeAllListeners(); + ViewHelper.setTranslationX(view, 0); + dispatchRemoveFinished(holder); + mRemoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + + @Override + public void onAnimationCancel(Animator animation) { + + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + } + ); + objAnimator.start(); + } + + @Override + public boolean animateAdd(final ViewHolder holder) { + endAnimation(holder); + ViewHelper.setAlpha(holder.itemView, 0f); + mPendingAdditions.add(holder); + return true; + } + + private void animateAddImpl(final ViewHolder holder) { + final View view = holder.itemView; + mAddAnimations.add(holder); + final ValueAnimator objAnimator = ObjectAnimator.ofFloat(view, "alpha", 1f); + objAnimator.setDuration(getAddDuration()); + objAnimator.addListener( + new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + dispatchAddStarting(holder); + } + + @Override + public void onAnimationEnd(Animator animation) { + animation.removeAllListeners(); + dispatchAddFinished(holder); + mAddAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + + @Override + public void onAnimationCancel(Animator animation) { + ViewHelper.setAlpha(view, 1f); + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + } + ); + objAnimator.start(); + } + + @Override + public boolean animateMove(final ViewHolder holder, int fromX, int fromY, + int toX, int toY) { + final View view = holder.itemView; + fromX += ViewCompat.getTranslationX(holder.itemView); + fromY += ViewCompat.getTranslationY(holder.itemView); + endAnimation(holder); + int deltaX = toX - fromX; + int deltaY = toY - fromY; + if (deltaX == 0 && deltaY == 0) { + dispatchMoveFinished(holder); + return false; + } + if (deltaX != 0) { + ViewCompat.setTranslationX(view, -deltaX); + } + if (deltaY != 0) { + ViewCompat.setTranslationY(view, -deltaY); + } + mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); + return true; + } + + private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) { + final View view = holder.itemView; + final int deltaX = toX - fromX; + final int deltaY = toY - fromY; + if (deltaX != 0) { + ViewCompat.animate(view).translationX(0); + } + if (deltaY != 0) { + ViewCompat.animate(view).translationY(0); + } + // TODO: make EndActions end listeners instead, since end actions aren't called when + // vpas are canceled (and can't end them. why?) + // need listener functionality in VPACompat for this. Ick. + mMoveAnimations.add(holder); + final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchMoveStarting(holder); + } + @Override + public void onAnimationCancel(View view) { + if (deltaX != 0) { + ViewCompat.setTranslationX(view, 0); + } + if (deltaY != 0) { + ViewCompat.setTranslationY(view, 0); + } + } + @Override + public void onAnimationEnd(View view) { + animation.setListener(null); + dispatchMoveFinished(holder); + mMoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView); + final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView); + final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); + endAnimation(oldHolder); + int deltaX = (int) (toX - fromX - prevTranslationX); + int deltaY = (int) (toY - fromY - prevTranslationY); + // recover prev translation state after ending animation + ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX); + ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY); + ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); + if (newHolder != null && newHolder.itemView != null) { + // carry over translation values + endAnimation(newHolder); + ViewCompat.setTranslationX(newHolder.itemView, -deltaX); + ViewCompat.setTranslationY(newHolder.itemView, -deltaY); + ViewCompat.setAlpha(newHolder.itemView, 0); + } + mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); + return true; + } + + private void animateChangeImpl(final ChangeInfo changeInfo) { + final ViewHolder holder = changeInfo.oldHolder; + final View view = holder == null ? null : holder.itemView; + final ViewHolder newHolder = changeInfo.newHolder; + final View newView = newHolder != null ? newHolder.itemView : null; + if (view != null) { + mChangeAnimations.add(changeInfo.oldHolder); + final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration( + getChangeDuration()); + oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); + oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); + oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchChangeStarting(changeInfo.oldHolder, true); + } + + @Override + public void onAnimationEnd(View view) { + oldViewAnim.setListener(null); + ViewCompat.setAlpha(view, 1); + ViewCompat.setTranslationX(view, 0); + ViewCompat.setTranslationY(view, 0); + dispatchChangeFinished(changeInfo.oldHolder, true); + mChangeAnimations.remove(changeInfo.oldHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + if (newView != null) { + mChangeAnimations.add(changeInfo.newHolder); + final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView); + newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()). + alpha(1).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchChangeStarting(changeInfo.newHolder, false); + } + @Override + public void onAnimationEnd(View view) { + newViewAnimation.setListener(null); + ViewCompat.setAlpha(newView, 1); + ViewCompat.setTranslationX(newView, 0); + ViewCompat.setTranslationY(newView, 0); + dispatchChangeFinished(changeInfo.newHolder, false); + mChangeAnimations.remove(changeInfo.newHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + } + + private void endChangeAnimation(List infoList, ViewHolder item) { + for (int i = infoList.size() - 1; i >= 0; i--) { + ChangeInfo changeInfo = infoList.get(i); + if (endChangeAnimationIfNecessary(changeInfo, item)) { + if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { + infoList.remove(changeInfo); + } + } + } + } + + private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { + if (changeInfo.oldHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); + } + if (changeInfo.newHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); + } + } + private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) { + boolean oldItem = false; + if (changeInfo.newHolder == item) { + changeInfo.newHolder = null; + } else if (changeInfo.oldHolder == item) { + changeInfo.oldHolder = null; + oldItem = true; + } else { + return false; + } + ViewCompat.setAlpha(item.itemView, 1); + ViewCompat.setTranslationX(item.itemView, 0); + ViewCompat.setTranslationY(item.itemView, 0); + dispatchChangeFinished(item, oldItem); + return true; + } + + @Override + public void endAnimation(ViewHolder item) { + final View view = item.itemView; + // this will trigger end callback which should set properties to their target values. + ViewCompat.animate(view).cancel(); + // TODO if some other animations are chained to end, how do we cancel them as well? + for (int i = mPendingMoves.size() - 1; i >= 0; i--) { + MoveInfo moveInfo = mPendingMoves.get(i); + if (moveInfo.holder == item) { + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item); + mPendingMoves.remove(i); + } + } + endChangeAnimation(mPendingChanges, item); + if (mPendingRemovals.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchRemoveFinished(item); + } + if (mPendingAdditions.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + } + + for (int i = mChangesList.size() - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + endChangeAnimation(changes, item); + if (changes.isEmpty()) { + mChangesList.remove(i); + } + } + for (int i = mMovesList.size() - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + for (int j = moves.size() - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + if (moveInfo.holder == item) { + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(i); + } + break; + } + } + } + for (int i = mAdditionsList.size() - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + if (additions.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + if (additions.isEmpty()) { + mAdditionsList.remove(i); + } + } + } + + // animations should be ended by the cancel above. + if (mRemoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mRemoveAnimations list"); + } + + if (mAddAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mAddAnimations list"); + } + + if (mChangeAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mChangeAnimations list"); + } + + if (mMoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mMoveAnimations list"); + } + dispatchFinishedWhenDone(); + } + + @Override + public boolean isRunning() { + return (!mPendingAdditions.isEmpty() || + !mPendingChanges.isEmpty() || + !mPendingMoves.isEmpty() || + !mPendingRemovals.isEmpty() || + !mMoveAnimations.isEmpty() || + !mRemoveAnimations.isEmpty() || + !mAddAnimations.isEmpty() || + !mChangeAnimations.isEmpty() || + !mMovesList.isEmpty() || + !mAdditionsList.isEmpty() || + !mChangesList.isEmpty()); + } + + /** + * Check the state of currently pending and running animations. If there are none + * pending/running, call {@link #dispatchAnimationsFinished()} to notify any + * listeners. + */ + private void dispatchFinishedWhenDone() { + if (!isRunning()) { + dispatchAnimationsFinished(); + } + } + + @Override + public void endAnimations() { + int count = mPendingMoves.size(); + for (int i = count - 1; i >= 0; i--) { + MoveInfo item = mPendingMoves.get(i); + View view = item.holder.itemView; + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item.holder); + mPendingMoves.remove(i); + } + count = mPendingRemovals.size(); + for (int i = count - 1; i >= 0; i--) { + ViewHolder item = mPendingRemovals.get(i); + dispatchRemoveFinished(item); + mPendingRemovals.remove(i); + } + count = mPendingAdditions.size(); + for (int i = count - 1; i >= 0; i--) { + ViewHolder item = mPendingAdditions.get(i); + View view = item.itemView; + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + mPendingAdditions.remove(i); + } + count = mPendingChanges.size(); + for (int i = count - 1; i >= 0; i--) { + endChangeAnimationIfNecessary(mPendingChanges.get(i)); + } + mPendingChanges.clear(); + if (!isRunning()) { + return; + } + + int listCount = mMovesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + count = moves.size(); + for (int j = count - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + ViewHolder item = moveInfo.holder; + View view = item.itemView; + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(moveInfo.holder); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(moves); + } + } + } + listCount = mAdditionsList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + count = additions.size(); + for (int j = count - 1; j >= 0; j--) { + ViewHolder item = additions.get(j); + View view = item.itemView; + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + additions.remove(j); + if (additions.isEmpty()) { + mAdditionsList.remove(additions); + } + } + } + listCount = mChangesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + count = changes.size(); + for (int j = count - 1; j >= 0; j--) { + endChangeAnimationIfNecessary(changes.get(j)); + if (changes.isEmpty()) { + mChangesList.remove(changes); + } + } + } + + cancelAll(mRemoveAnimations); + cancelAll(mMoveAnimations); + cancelAll(mAddAnimations); + cancelAll(mChangeAnimations); + + dispatchAnimationsFinished(); + } + + void cancelAll(List viewHolders) { + for (int i = viewHolders.size() - 1; i >= 0; i--) { + ViewCompat.animate(viewHolders.get(i).itemView).cancel(); + } + } + + private static class VpaListenerAdapter implements ViewPropertyAnimatorListener { + @Override + public void onAnimationStart(View view) {} + + @Override + public void onAnimationEnd(View view) {} + + @Override + public void onAnimationCancel(View view) {} + }; +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java index e7518911a..0092f59f6 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java @@ -17,6 +17,8 @@ package com.hippo.ehviewer.widget; import android.content.Context; import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.os.Bundle; import android.os.Parcelable; import android.support.v7.widget.LinearLayoutManager; @@ -30,11 +32,13 @@ import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.TextView; import com.hippo.effect.ripple.RippleSalon; import com.hippo.ehviewer.R; import com.hippo.util.ViewUtils; +import com.hippo.widget.FloatLabelEditText; import com.hippo.widget.FloatingActionButton; import com.hippo.widget.PrefixEditText; import com.hippo.widget.recyclerview.EasyRecyclerView; @@ -45,12 +49,12 @@ public class SearchLayout extends FrameLayout implements CompoundButton.OnChecke View.OnClickListener { private static final String STATE_KEY_SUPER = "super"; - private static final String STATE_KEY_SEARCH_TYPE = "search_type"; + private static final String STATE_KEY_SEARCH_MODE = "search_mode"; private static final String STATE_KEY_ENABLE_ADVANCE = "enable_advance"; - private static final int SEARCH_TYPE_NORMAL = 0; - private static final int SEARCH_TYPE_TAG = 1; - private static final int SEARCH_TYPE_IMAGE = 2; + private static final int SEARCH_MODE_NORMAL = 0; + private static final int SEARCH_MODE_TAG = 1; + private static final int SEARCH_MODE_IMAGE = 2; private static final int ITEM_TYPE_NORMAL = 0; private static final int ITEM_TYPE_NORMAL_ADVANCE = 1; @@ -71,7 +75,7 @@ public class SearchLayout extends FrameLayout implements CompoundButton.OnChecke private Context mContext; private Resources mResources; - private int mSearchType = SEARCH_TYPE_NORMAL; + private int mSearchMode = SEARCH_MODE_NORMAL; private boolean mEnableAdvance = false; private EasyRecyclerView mSearchContainer; @@ -86,15 +90,27 @@ public class SearchLayout extends FrameLayout implements CompoundButton.OnChecke private View mAdvanceView; private AdvanceSearchTable mTableAdvanceSearch; + private FloatLabelEditText mEditTextTag; + + private View mImageView; + private TextView mTextSelectImage; + private TextView mTextImagePath; + private ImageView mImagePreview; + private View mActionView; private TextView mAction1; private TextView mAction2; private LinearLayoutManager mLayoutManager; private SearchAdapter mAdapter; + private SearchItemAnimator mAnimator; private SparseArray mSavedInstanceState; + private SearhLayoutHelper mHelper; + + private Bitmap mSearchImage; + public SearchLayout(Context context) { super(context); init(context); @@ -121,9 +137,15 @@ public class SearchLayout extends FrameLayout implements CompoundButton.OnChecke mLayoutManager = new LinearLayoutManager(mContext); mAdapter = new SearchAdapter(); + mAnimator = new SearchItemAnimator(mSearchContainer); mSearchContainer.setLayoutManager(mLayoutManager); mSearchContainer.setAdapter(mAdapter); mSearchContainer.setHasFixedSize(true); + mSearchContainer.setItemAnimator(mAnimator); + } + + public void setHelper(SearhLayoutHelper helper) { + mHelper = helper; } @Override @@ -144,7 +166,7 @@ public class SearchLayout extends FrameLayout implements CompoundButton.OnChecke public Parcelable onSaveInstanceState() { final Bundle state = new Bundle(); state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState()); - state.putInt(STATE_KEY_SEARCH_TYPE, mSearchType); + state.putInt(STATE_KEY_SEARCH_MODE, mSearchMode); state.putBoolean(STATE_KEY_ENABLE_ADVANCE, mEnableAdvance); return state; } @@ -154,7 +176,7 @@ public class SearchLayout extends FrameLayout implements CompoundButton.OnChecke if (state instanceof Bundle) { final Bundle savedState = (Bundle) state; super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER)); - mSearchType = savedState.getInt(STATE_KEY_SEARCH_TYPE); + mSearchMode = savedState.getInt(STATE_KEY_SEARCH_MODE); mEnableAdvance = savedState.getBoolean(STATE_KEY_ENABLE_ADVANCE); } } @@ -166,7 +188,7 @@ public class SearchLayout extends FrameLayout implements CompoundButton.OnChecke } else if (buttonView == mSwitchEnableAdvance) { mEnableAdvance = isChecked; - if (mSearchType == SEARCH_TYPE_NORMAL) { + if (mSearchMode == SEARCH_MODE_NORMAL) { if (isChecked) { mAdapter.notifyItemInserted(1); } else { @@ -176,9 +198,44 @@ public class SearchLayout extends FrameLayout implements CompoundButton.OnChecke } } + private void taggleSearchMode() { + int oldItemCount = mAdapter.getItemCount(); + + mSearchMode++; + if (mSearchMode > SEARCH_MODE_IMAGE) { + mSearchMode = SEARCH_MODE_NORMAL; + } + + int newItemCount = mAdapter.getItemCount(); + + mAdapter.notifyItemRangeRemoved(0, oldItemCount - 1); + mAdapter.notifyItemRangeInserted(0, newItemCount - 1); + } + @Override public void onClick(View v) { + if (v == mAction1) { + // TODO add quick search + } else if (v == mAction2) { + taggleSearchMode(); + } else if (v == mTextSelectImage) { + if (mHelper != null) { + mHelper.requestSelectImage(); + } + } + } + public void onSelectImage(@NotNull String imagePath) { + mTextImagePath.setText(imagePath); + + if (mSearchImage != null && !mSearchImage.isRecycled()) { + mSearchImage.isRecycled(); + } + mSearchImage = BitmapFactory.decodeFile(imagePath); + mImagePreview.setImageBitmap(mSearchImage); + + ViewUtils.setVisibility(mTextImagePath, View.VISIBLE); + ViewUtils.setVisibility(mImagePreview, View.VISIBLE); } private class SearchHolder extends RecyclerView.ViewHolder { @@ -217,8 +274,8 @@ public class SearchLayout extends FrameLayout implements CompoundButton.OnChecke @Override public int getItemCount() { - int count = SEARCH_ITEM_COUNT_ARRAY[mSearchType]; - if (mSearchType == SEARCH_TYPE_NORMAL && !mEnableAdvance) { + int count = SEARCH_ITEM_COUNT_ARRAY[mSearchMode]; + if (mSearchMode == SEARCH_MODE_NORMAL && !mEnableAdvance) { count--; } return count; @@ -226,8 +283,8 @@ public class SearchLayout extends FrameLayout implements CompoundButton.OnChecke @Override public int getItemViewType(int position) { - int type = SEARCH_ITEM_TYPE[mSearchType][position]; - if (mSearchType == SEARCH_TYPE_NORMAL && position == 1 && !mEnableAdvance) { + int type = SEARCH_ITEM_TYPE[mSearchMode][position]; + if (mSearchMode == SEARCH_MODE_NORMAL && position == 1 && !mEnableAdvance) { type = ITEM_TYPE_ACTION; } return type; @@ -316,10 +373,43 @@ public class SearchLayout extends FrameLayout implements CompoundButton.OnChecke private void bindTagView(SearchHolder holder) { holder.title.setText(R.string.search_tag); + + if (mEditTextTag == null) { + mInflater.inflate(R.layout.search_tag, holder.content); + mEditTextTag = (FloatLabelEditText) holder.content.getChildAt(0); + + // Restore state + if (mSavedInstanceState != null) { + holder.content.restoreHierarchyState(mSavedInstanceState); + } + } else { + ViewUtils.removeFromParent(mEditTextTag); + holder.content.addView(mEditTextTag); + } } private void bindImageView(SearchHolder holder) { holder.title.setText(R.string.search_image); + + if (mImageView == null) { + mInflater.inflate(R.layout.search_image, holder.content); + mImageView = holder.content.getChildAt(0); + + mTextSelectImage = (TextView) mImageView.findViewById(R.id.search_select_image_action); + mTextImagePath = (TextView) mImageView.findViewById(R.id.search_select_image_path); + mImagePreview = (ImageView) mImageView.findViewById(R.id.search_select_image_image); + + // Restore state + if (mSavedInstanceState != null) { + holder.content.restoreHierarchyState(mSavedInstanceState); + } + + RippleSalon.addRipple(mTextSelectImage, false); + mTextSelectImage.setOnClickListener(SearchLayout.this); + } else { + ViewUtils.removeFromParent(mImageView); + holder.content.addView(mImageView); + } } private void bindActionView(ActionHolder holder) { @@ -341,4 +431,8 @@ public class SearchLayout extends FrameLayout implements CompoundButton.OnChecke // Empty, bind view in create view } } + + public interface SearhLayoutHelper { + public void requestSelectImage(); + } } diff --git a/app/src/main/java/com/hippo/scene/Scene.java b/app/src/main/java/com/hippo/scene/Scene.java index 0d288a13a..a4a721f70 100644 --- a/app/src/main/java/com/hippo/scene/Scene.java +++ b/app/src/main/java/com/hippo/scene/Scene.java @@ -15,6 +15,7 @@ package com.hippo.scene; +import android.content.Intent; import android.os.Bundle; import android.os.Parcelable; import android.support.v4.app.FragmentManager; @@ -128,6 +129,10 @@ public abstract class Scene { } } + public void startActivityForResult(Intent intent, ActivityResultListener listener) { + mStageActivity.startActivityForResult(intent, listener); + } + /** * Return the FragmentManager for interacting with fragments associated * with StageActivity. @@ -135,4 +140,8 @@ public abstract class Scene { public FragmentManager getSupportFragmentManager() { return mStageActivity.getSupportFragmentManager(); } + + public interface ActivityResultListener { + public void onGetResult(int resultCode, Intent data); + } } diff --git a/app/src/main/java/com/hippo/scene/StageActivity.java b/app/src/main/java/com/hippo/scene/StageActivity.java index d792a79d9..7de4f2801 100644 --- a/app/src/main/java/com/hippo/scene/StageActivity.java +++ b/app/src/main/java/com/hippo/scene/StageActivity.java @@ -15,9 +15,12 @@ package com.hippo.scene; +import android.content.Intent; import android.os.Bundle; +import android.util.SparseArray; import com.hippo.ehviewer.ui.AbsActionBarActivity; +import com.hippo.util.IntIdGenerator; import java.util.LinkedList; import java.util.Stack; @@ -27,9 +30,12 @@ public abstract class StageActivity extends AbsActionBarActivity { private Stack mSceneStack = new Stack<>(); private LinkedList mScenseActionQueue = new LinkedList<>(); private Scene mCurrentScene = null; - private Scene mRetainedScene = null; + private IntIdGenerator mActivityResultIdGenerator = IntIdGenerator.create(); + private SparseArray mActivityResultListenerMap = + new SparseArray<>(); + public abstract StageLayout getStageView(); private void pushScene(Class sceneClass) { @@ -148,6 +154,23 @@ public abstract class StageActivity extends AbsActionBarActivity { } } + public void startActivityForResult(Intent intent, Scene.ActivityResultListener listener) { + int id = mActivityResultIdGenerator.nextId(); + mActivityResultListenerMap.put(id, listener); + startActivityForResult(intent, id); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + Scene.ActivityResultListener listener = mActivityResultListenerMap.get(requestCode); + if (listener != null) { + listener.onGetResult(resultCode, data); + } + mActivityResultListenerMap.delete(requestCode); + } + @Override public void onBackPressed() { if (mCurrentScene != null) { diff --git a/app/src/main/java/com/hippo/util/IntIdGenerator.java b/app/src/main/java/com/hippo/util/IntIdGenerator.java new file mode 100644 index 000000000..7fc9c3507 --- /dev/null +++ b/app/src/main/java/com/hippo/util/IntIdGenerator.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hippo.util; + +public final class IntIdGenerator { + + private int mId = 0; + + private IntIdGenerator() { + // Empty + } + + public int nextId() { + return mId++; + } + + public static IntIdGenerator create() { + return new IntIdGenerator(); + } +} diff --git a/app/src/main/java/com/hippo/widget/FloatLabelEditText.java b/app/src/main/java/com/hippo/widget/FloatLabelEditText.java index 4f5557d87..ab8de8e77 100644 --- a/app/src/main/java/com/hippo/widget/FloatLabelEditText.java +++ b/app/src/main/java/com/hippo/widget/FloatLabelEditText.java @@ -57,7 +57,7 @@ public class FloatLabelEditText extends FrameLayout { private Trigger mTrigger = Trigger.TEXT; - private String mHint; + private CharSequence mHint; public FloatLabelEditText(Context context, AttributeSet attrs) { super(context, attrs); @@ -84,7 +84,6 @@ public class FloatLabelEditText extends FrameLayout { mLabel.setTextAppearance(context, a.getResourceId(R.styleable.FloatLabelEditText_fllLabelAppearance, android.R.style.TextAppearance_Small)); - mHint = a.getString(R.styleable.FloatLabelEditText_fllHint); final int index = a.getInt(R.styleable.FloatLabelEditText_fllTrigger, -1); if (index >= 0) { setTrigger(sTriggerArray[index]); @@ -92,8 +91,14 @@ public class FloatLabelEditText extends FrameLayout { a.recycle(); addView(mLabel, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + EditText attrCollector = new EditText(context, attrs); mEditText = (EditText) LayoutInflater.from(getContext()).inflate( R.layout.widget_float_label_edit_text, this, false); + + // Collect attr + mHint = attrCollector.getHint(); + mEditText.setInputType(attrCollector.getInputType()); + final LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); lp.gravity = Gravity.BOTTOM; lp.topMargin = (int) mLabel.getTextSize(); diff --git a/app/src/main/res/color-v11/float_label_edit_text_light.xml b/app/src/main/res/color-v11/float_label_edit_text_light.xml new file mode 100644 index 000000000..bbcf73f26 --- /dev/null +++ b/app/src/main/res/color-v11/float_label_edit_text_light.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/app/src/main/res/color/float_label_edit_text_light.xml b/app/src/main/res/color/float_label_edit_text_light.xml new file mode 100644 index 000000000..22e748068 --- /dev/null +++ b/app/src/main/res/color/float_label_edit_text_light.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/layout-sw600dp/search_action.xml b/app/src/main/res/layout-sw600dp/search_action.xml index 6a79b27c2..9d02e5c16 100644 --- a/app/src/main/res/layout-sw600dp/search_action.xml +++ b/app/src/main/res/layout-sw600dp/search_action.xml @@ -28,14 +28,14 @@ diff --git a/app/src/main/res/layout/search_action.xml b/app/src/main/res/layout/search_action.xml index c685e4205..72b79d92a 100644 --- a/app/src/main/res/layout/search_action.xml +++ b/app/src/main/res/layout/search_action.xml @@ -22,7 +22,7 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/search_normal.xml b/app/src/main/res/layout/search_normal.xml index 2920e57ec..2c40df68d 100644 --- a/app/src/main/res/layout/search_normal.xml +++ b/app/src/main/res/layout/search_normal.xml @@ -37,7 +37,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/search_item_interval" - android:hint="@string/hint_no_keyword" + android:hint="@string/search_hint_no_keyword" android:singleLine="true"/> + + diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml index ca9b80e8f..aaab676a4 100644 --- a/app/src/main/res/values-sw600dp/dimens.xml +++ b/app/src/main/res/values-sw600dp/dimens.xml @@ -46,6 +46,9 @@ 8dp + + 16dp + 52dp 52dp diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6a0515113..7fcb562f7 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -22,7 +22,7 @@ 标签搜索 图片搜索 指定上传者 - 没关键字也行 + 没关键字也行 启用高级选项 搜索画廊 @@ -35,6 +35,10 @@ 显示 Expunged 画廊 最低评分: + 标签 + + 选择图片 + 添加 模式 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 0c56ac1b7..a9fb5f9e8 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -30,7 +30,6 @@ - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 4eee30f56..b05fe31cf 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -50,6 +50,8 @@ 4dp + + 8dp 0dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 297d192f3..8d8355ecb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,7 +39,7 @@ Tag Image Specify Uploader - No keyword is OK + No keyword is OK Enable advance options Search Gallery @@ -52,6 +52,10 @@ Show Expunged Galleries Minimum Rating: + Tag + + Select image + Add Mode diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index fee4d0f92..3981ba81a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -34,6 +34,15 @@ 44dp + + + + + -