package com.aincc.libtest.activity.flip; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PixelFormat; import android.graphics.Rect; import android.opengl.GLSurfaceView; import android.os.Handler; import android.os.Message; import android.os.Parcel; import android.os.Parcelable; import android.support.v4.os.ParcelableCompat; import android.support.v4.os.ParcelableCompatCreatorCallbacks; import android.support.v4.view.PagerAdapter; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import com.aincc.lib.util.Logger; import com.aincc.libtest.activity.flip.internal.FlipRenderer; /* Copyright 2012 Aphid Mobile 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 distributed under the License is distributed on an "AS IS" BASIS, 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. */ /** * * <h3><b>FlipViewGroup</b></h3></br> * * 플립보드 스타일의 애니메이션 효과를 표시하는 레이아웃 * <p> * * @author aincc@barusoft.com * @version 1.0.0 * @since 1.0.0 */ public class FlipViewGroup extends ViewGroup { /** * SurfaceView 생성 메시지 */ private static final int MSG_SURFACE_CREATED = 1; /** * Populate 요청 메시지 */ private static final int MSG_POPULATE = 2; /** * 레이아웃에서 유지중인 플립뷰 관리 */ @SuppressLint("UseSparseArrays") private final Map<Integer, View> flipviews = new HashMap<Integer, View>(); /** * 플립아이템 */ private final ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); /** * 핸들러 */ private Handler handler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { if (msg.what == MSG_SURFACE_CREATED) { Logger.v("FlipViewGroup::handleMessage() MSG_SURFACE_CREATED"); requestLayout(); return true; } else if (msg.what == MSG_POPULATE) { Logger.v("FlipViewGroup::handleMessage() MSG_POPULATE"); populate(); return true; } return false; } }); /** * 어댑터 */ private FlipAdapter adapter; /** * 옵저버 */ private FlipAdapter.DataSetObserver observer; /** * 페이지 변경 리스너 */ private OnPageChangeListener pageChangeListener; /** * Surface 뷰 */ private GLSurfaceView surfaceView; /** * 렌더러 */ private FlipRenderer renderer; /** * 이전 페이지 인덱스 */ private int prevItem = -1; /** * 현재 페이지 인덱스 */ private int currentItem = -1; /** * 복구된 현재 페이지 인덱스 */ private int restoredCurrentItem = -1; /** * 복구된 어댑터 상태 */ private Parcelable restoredAdapterState = null; /** * 복구된 클래스 로더 */ private ClassLoader restoredClassLoader = null; /** * */ private int childWidthMeasureSpec; /** * */ private int childHeightMeasureSpec; /** * 레이아웃 그리기 상태 */ private boolean inLayout; /** * 레이아웃 전개 지연 상태 여부 */ private boolean populatePending; /** * 최초 레이아웃 그리기 여부 */ private boolean firstLayout = true; /** * * @since 1.0.0 * @param context */ public FlipViewGroup(Context context) { super(context); initFlipViewGroup(); } /** * * @since 1.0.0 * @param context * @param attr */ public FlipViewGroup(Context context, AttributeSet attr) { super(context, attr); initFlipViewGroup(); } /** * * @since 1.0.0 * @param context * @param attr * @param defStyle */ public FlipViewGroup(Context context, AttributeSet attr, int defStyle) { super(context, attr, defStyle); initFlipViewGroup(); } /** * @return the SurfaceView */ public GLSurfaceView getSurfaceView() { return surfaceView; } /** * @return the Renderer */ public FlipRenderer getRenderer() { return renderer; } /** * @since 1.0.0 */ public void onResume() { surfaceView.onResume(); } /** * @since 1.0.0 */ public void onPause() { surfaceView.onPause(); } /** * 어댑터 설정 * * @since 1.0.0 * @param adapter */ public void setAdapter(FlipAdapter adapter) { // 이전 어댑터의 내용을 삭제하고 모든 뷰를 삭제한다. if (adapter != null) { adapter.setDataSetObserver(null); adapter.startUpdate(this); for (int i = 0; i < items.size(); i++) { final ItemInfo ii = items.get(i); adapter.destroyItem(this, ii.position, ii.object); } adapter.finishUpdate(this); items.clear(); removeAllViews(); // remove all view include SurfaceView currentItem = 0; } // Surface 뷰를 추가한다. setupSurfaceView(); // 어댑터를 설정한다. this.adapter = adapter; if (adapter != null) { if (observer == null) { observer = new DataSetObserver(); } adapter.setDataSetObserver(observer); populatePending = false; if (restoredCurrentItem >= 0) { adapter.restoreState(restoredAdapterState, restoredClassLoader); setCurrentItemInternal(restoredCurrentItem, true); restoredCurrentItem = -1; restoredAdapterState = null; restoredClassLoader = null; } else { // 화면표시를 위한 뷰 초기화 작업을 요청한다. populate(); } } } /** * * @since 1.0.0 * @return the adapter */ public FlipAdapter getAdapter() { return adapter; } /** * * @since 1.0.0 * @param listener */ public void setOnPageChangeListener(OnPageChangeListener listener) { pageChangeListener = listener; } // TODO: 외부에서 이를 설정할 경우 해당 페이지로 솨라락 넘어가는 효과를 보이면서 가게 할 수 있을까? /** * @param item * Item index to select */ public void setCurrentItem(int item) { prevItem = currentItem; populatePending = false; setCurrentItemInternal(item, false); } /** * * @since 1.0.0 * @return the current item index */ public int getCurrentItem() { return currentItem; } /** * Surface 뷰의 생성완료로 화면 갱신 요청을 전달한다. * * @since 1.0.0 */ public void reloadTexture() { handler.sendMessage(Message.obtain(handler, MSG_SURFACE_CREATED)); } /** * 화면표시를 위한 뷰 초기화 작업 요청을 UIThread로 전달한다. * * @since 1.0.0 */ public void requestPopulate() { handler.sendMessage(Message.obtain(handler, MSG_POPULATE)); } /** * 플립뷰 초기화 * * @since 1.0.0 */ private void initFlipViewGroup() { Logger.v("FlipViewGroup::initFlipViewGroup()"); // TODO: 초기설정할 내용을 추가한다. } /** * Surface 뷰 초기화<br> * 어댑터를 설정할 때 생성하여 추가한다. * * @since 1.0.0 */ private void setupSurfaceView() { Logger.v("FlipViewGroup::setupSurfaceView()"); surfaceView = new GLSurfaceView(getContext()); renderer = new FlipRenderer(this); surfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); surfaceView.setZOrderOnTop(true); surfaceView.setRenderer(renderer); // surfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT); // 레이어 투과 효과 surfaceView.getHolder().setFormat(PixelFormat.OPAQUE); surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); renderer.getCards().setFlipViewGroup(this); addView(surfaceView); } /** * 현재 아이템을 설정하고 UIThread로 화면을 그리기 위한 뷰 초기화 작업을 요청한다. * * @since 1.0.0 * @param item * @param always */ private void setCurrentItemInternal(int item, boolean always) { if (adapter == null || adapter.getCount() <= 0) { return; } if (!always && currentItem == item && items.size() != 0) { return; } if (item < 0) { item = 0; } else if (item >= adapter.getCount()) { item = adapter.getCount() - 1; } currentItem = item; requestPopulate(); } /** * 신규 페이지 정보 추가 및 생성 * * @since 1.0.0 * @param position * @param index */ private void addNewItem(int position, int index) { ItemInfo ii = new ItemInfo(); ii.position = position; ii.object = adapter.instantiateItem(this, position); if (index < 0) { items.add(ii); } else { items.add(index, ii); } } /** * 데이터 변경 시 화면 갱신 처리 * * @since 1.0.0 */ private void dataSetChanged() { boolean needPopulate = items.size() < 3 && items.size() < adapter.getCount(); int newCurrItem = -1; for (int i = 0; i < items.size(); i++) { final ItemInfo ii = items.get(i); final int newPos = adapter.getItemPosition(ii.object); if (newPos == PagerAdapter.POSITION_UNCHANGED) { continue; } if (newPos == PagerAdapter.POSITION_NONE) { items.remove(i); i--; adapter.destroyItem(this, ii.position, ii.object); needPopulate = true; if (currentItem == ii.position) { // Keep the current item in the valid range newCurrItem = Math.max(0, Math.min(currentItem, adapter.getCount() - 1)); } continue; } if (ii.position != newPos) { if (ii.position == currentItem) { // Our current item changed position. Follow it. newCurrItem = newPos; } ii.position = newPos; needPopulate = true; } } Collections.sort(items, COMPARATOR); if (newCurrItem >= 0) { // TODO This currently causes a jump. setCurrentItemInternal(newCurrItem, true); needPopulate = true; } if (needPopulate) { populate(); requestLayout(); } } /** * 화면 표시를 위한 뷰 초기화 작업을 처리한다. * * @since 1.0.0 */ private void populate() { if (adapter == null) { return; } if (populatePending) { Logger.i("FlipViewGroup::populate is pending, skipping for now..."); return; } if (getWindowToken() == null) { return; } // 시작 adapter.startUpdate(this); final int startPos = Math.max(0, currentItem - 2); final int N = adapter.getCount(); final int endPos = Math.min(N - 1, currentItem + 2); Logger.v("FlipViewGroup::populating: startPos=" + startPos + " endPos=" + endPos); // Add and remove pages in the existing list. int lastPos = -1; for (int i = 0; i < items.size(); i++) { ItemInfo ii = items.get(i); if (ii.position < startPos || ii.position > endPos) { Logger.i("FlipViewGroup::removing: " + ii.position + " @ " + i); items.remove(i); i--; adapter.destroyItem(this, ii.position, ii.object); } else if (lastPos < endPos && ii.position > startPos) { // The next item is outside of our range, but we have a gap // between it and the last item where we want to have a page // shown. Fill in the gap. lastPos++; if (lastPos < startPos) { lastPos = startPos; } while (lastPos <= endPos && lastPos < ii.position) { Logger.i("FlipViewGroup::inserting: " + lastPos + " @ " + i); addNewItem(lastPos, i); lastPos++; i++; } } lastPos = ii.position; } // Add any new pages we need at the end. lastPos = items.size() > 0 ? items.get(items.size() - 1).position : -1; if (lastPos < endPos) { lastPos++; lastPos = lastPos > startPos ? lastPos : startPos; while (lastPos <= endPos) { Logger.i("FlipViewGroup::appending: " + lastPos); addNewItem(lastPos, -1); lastPos++; } } if (Logger.isDebug) { Logger.i("FlipViewGroup::Current page list:"); for (int i = 0; i < items.size(); i++) { Logger.i("FlipViewGroup::#" + i + ": page " + items.get(i).position); } } ItemInfo curItem = null; for (int i = 0; i < items.size(); i++) { if (items.get(i).position == currentItem) { curItem = items.get(i); break; } } adapter.setPrimaryItem(this, currentItem, curItem != null ? curItem.object : null); // 완료 adapter.finishUpdate(this); if (hasFocus()) { View currentFocused = findFocus(); ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; if (ii == null || ii.position != currentItem) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); ii = infoForChild(child); if (ii != null && ii.position == currentItem) { if (child.requestFocus(FOCUS_FORWARD)) { break; } } } } } } /** * @return 현재페이지의 이전 이전 페이지 뷰 반환 */ private View getPrevPrevView() { return flipviews.get(currentItem - 2); } /** * @return 현재페이지의 이전 페이지 뷰 반환 */ private View getPrevView() { return flipviews.get(currentItem - 1); } /** * @return 현재페이지 뷰 반환 */ private View getCurrentView() { return flipviews.get(currentItem); } /** * @return 현재페이지의 다음 페이지 뷰 반환 */ private View getNextView() { return flipviews.get(currentItem + 1); } /** * @return 현재페이지의 다음 다음 페이지 뷰 반환 */ private View getNextNextView() { return flipviews.get(currentItem + 2); } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { int tempCurrentViewId = currentItem; if (event.getY() < getHeight() / 2) { if (tempCurrentViewId > 0) { tempCurrentViewId--; } renderer.getCards().handleTouchEventDown(true, tempCurrentViewId, getPrevPrevView()); } else { if (tempCurrentViewId < adapter.getCount() - 1) { tempCurrentViewId++; } renderer.getCards().handleTouchEventDown(false, tempCurrentViewId, getNextNextView()); } } return renderer.getCards().handleTouchEvent(event); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { Logger.i("--------------------------------------------------------------------"); Logger.i("FlipViewGroup::onLayout() " + changed + " " + l + ", " + t + ", " + r + ", " + b + "; child " + (null != adapter ? adapter.getCount() : 0)); inLayout = true; populate(); inLayout = false; final int count = getChildCount(); flipviews.clear(); for (int ii = 0; ii < count; ii++) { View child = getChildAt(ii); ItemInfo info = null; if (child.getVisibility() != GONE && (info = infoForChild(child)) != null) { child.layout(0, 0, r - l, b - t); } if (null != info) { flipviews.put(Integer.valueOf(info.position), child); } } int w = r - l; int h = b - t; surfaceView.layout(0, 0, w, h); if (null != adapter && adapter.getCount() >= 2) { Logger.i("FlipViewGroup::onLayout() firstLayout = " + firstLayout); if (firstLayout) { firstLayout = renderer.updateTexture(getCurrentView(), getPrevView(), getNextView()); renderer.getCards().reloadFirstTexture(); } } if (null != pageChangeListener && renderer.isCreated()) { if (prevItem != currentItem) { pageChangeListener.onPageSelected(currentItem); } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec)); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY); inLayout = true; populate(); inLayout = false; final int size = getChildCount(); for (int i = 0; i < size; ++i) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } } @Override public void addView(View child, int index, LayoutParams params) { if (inLayout) { addViewInLayout(child, index, params); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } else { super.addView(child, index, params); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); firstLayout = true; } /** * We only want the current page that is being shown to be focusable. */ @Override public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { final int focusableCount = views.size(); final int descendantFocusability = getDescendantFocusability(); if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { ItemInfo ii = infoForChild(child); if (ii != null && ii.position == currentItem) { child.addFocusables(views, direction, focusableMode); } } } } // we add ourselves (if focusable) in all cases except for when we are // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is // to avoid the focus search finding layouts when a more precise search // among the focusable children would be more interesting. if (descendantFocusability != FOCUS_AFTER_DESCENDANTS || // No focusable descendants (focusableCount == views.size())) { // Note that we can't call the superclass here, because it will // add all views in. So we need to do the same thing View does. if (!isFocusable()) { return; } if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && isInTouchMode() && !isFocusableInTouchMode()) { return; } if (views != null) { views.add(this); } } } /** * We only want the current page that is being shown to be touchable. */ @Override public void addTouchables(ArrayList<View> views) { // Note that we don't call super.addTouchables(), which means that // we don't call View.addTouchables(). This is okay because a ViewPager // is itself not touchable. for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { ItemInfo ii = infoForChild(child); if (ii != null && ii.position == currentItem) { child.addTouchables(views); } } } } /** * We only want the current page that is being shown to be focusable. */ @Override protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { int index; int increment; int end; int count = getChildCount(); if ((direction & FOCUS_FORWARD) != 0) { index = 0; increment = 1; end = count; } else { index = count - 1; increment = -1; end = -1; } for (int i = index; i != end; i += increment) { View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { ItemInfo ii = infoForChild(child); if (ii != null && ii.position == currentItem) { if (child.requestFocus(direction, previouslyFocusedRect)) { return true; } } } } return false; } @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { // ViewPagers should only report accessibility info for the current page, // otherwise things get very confusing. // TODO: Should this note something about the paging container? final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { final ItemInfo ii = infoForChild(child); if (ii != null && ii.position == currentItem && child.dispatchPopulateAccessibilityEvent(event)) { return true; } } } return false; } /** * * <h3><b>DataSetObserver</b></h3></br> * * @author aincc@barusoft.com * @version 1.0.0 * @since 1.0.0 */ private class DataSetObserver implements FlipAdapter.DataSetObserver { @Override public void onDataSetChanged() { dataSetChanged(); } } /** * 정렬비교 */ private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>() { @Override public int compare(ItemInfo lhs, ItemInfo rhs) { return lhs.position - rhs.position; } }; /** * * <h3><b>ItemInfo</b></h3></br> * * @author aincc@barusoft.com * @version 1.0.0 * @since 1.0.0 */ static class ItemInfo { Object object; int position; boolean flipping; } /** * 지정한 자식뷰에 해당하는 ItemInfo 정보 가져오기 * * @since 1.0.0 * @param child * @return the ItemInfo */ ItemInfo infoForChild(View child) { for (int i = 0; i < items.size(); i++) { ItemInfo ii = items.get(i); if (adapter.isViewFromObject(child, ii.object)) { return ii; } } return null; } /** * * @since 1.0.0 * @param child * @return */ ItemInfo infoForAnyChild(View child) { ViewParent parent; while ((parent = child.getParent()) != this) { if (parent == null || !(parent instanceof View)) { return null; } child = (View) parent; } return infoForChild(child); } /** * * <h3><b>OnPageChangeListener</b></h3></br> * * @author aincc@barusoft.com * @version 1.0.0 * @since 1.0.0 */ public interface OnPageChangeListener { /** * 새로운 페이지가 선택된 경우 호출된다. * * @param position */ public void onPageSelected(int position); } /** * Simple implementation of the {@link OnPageChangeListener} interface with stub * implementations of each method. Extend this if you do not intend to override * every method of {@link OnPageChangeListener}. */ public static class SimpleOnPageChangeListener implements OnPageChangeListener { @Override public void onPageSelected(int position) { // This space for rent } } /** * * <h3><b>SavedState</b></h3></br> * * @author aincc@barusoft.com * @version 1.0.0 * @since 1.0.0 */ public static class SavedState extends BaseSavedState { int position; Parcelable adapterState; ClassLoader loader; public SavedState(Parcelable superState) { super(superState); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(position); out.writeParcelable(adapterState, flags); } @Override public String toString() { return "FragmentPager.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " position=" + position + "}"; } public static final Parcelable.Creator<SavedState> CREATOR = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() { @Override public SavedState createFromParcel(Parcel in, ClassLoader loader) { return new SavedState(in, loader); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }); SavedState(Parcel in, ClassLoader loader) { super(in); if (loader == null) { loader = getClass().getClassLoader(); } position = in.readInt(); adapterState = in.readParcelable(loader); this.loader = loader; } } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.position = currentItem; if (adapter != null) { ss.adapterState = adapter.saveState(); } return ss; } @Override public void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); if (adapter != null) { adapter.restoreState(ss.adapterState, ss.loader); setCurrentItemInternal(ss.position, true); } else { restoredCurrentItem = ss.position; restoredAdapterState = ss.adapterState; restoredClassLoader = ss.loader; } } }