From 2897a2fe12b8ccbe0a508aaf5a388086384fb7d4 Mon Sep 17 00:00:00 2001 From: seven332 Date: Sun, 7 Feb 2016 23:19:52 +0800 Subject: [PATCH] Add gl file --- app/src/main/AndroidManifest.xml | 7 + .../hippo/ehviewer/ui/GalleryActivity.java | 49 + .../hippo/ehviewer/ui/gl/DownUpDetector.java | 67 + .../hippo/ehviewer/ui/gl/GalleryPageView.java | 124 ++ .../com/hippo/ehviewer/ui/gl/GalleryView.java | 1396 +++++++++++++++++ .../ehviewer/ui/gl/GestureRecognizer.java | 163 ++ .../com/hippo/ehviewer/ui/gl/ImageView.java | 45 + .../com/hippo/gl/anim/AlphaAnimation.java | 48 + .../java/com/hippo/gl/anim/Animation.java | 166 ++ .../com/hippo/gl/anim/CanvasAnimation.java | 25 + .../com/hippo/gl/anim/FloatAnimation.java | 40 + .../com/hippo/gl/glrenderer/BasicTexture.java | 222 +++ .../hippo/gl/glrenderer/BitmapTexture.java | 54 + .../hippo/gl/glrenderer/CanvasTexture.java | 51 + .../com/hippo/gl/glrenderer/ColorTexture.java | 74 + .../com/hippo/gl/glrenderer/ExtTexture.java | 60 + .../hippo/gl/glrenderer/FadeInTexture.java | 54 + .../hippo/gl/glrenderer/FadeOutTexture.java | 54 + .../com/hippo/gl/glrenderer/FadeTexture.java | 81 + .../com/hippo/gl/glrenderer/GLCanvas.java | 223 +++ .../com/hippo/gl/glrenderer/GLES20Canvas.java | 1072 +++++++++++++ .../com/hippo/gl/glrenderer/GLES20IdImpl.java | 42 + .../java/com/hippo/gl/glrenderer/GLId.java | 33 + .../java/com/hippo/gl/glrenderer/GLPaint.java | 50 + .../com/hippo/gl/glrenderer/ImageTexture.java | 687 ++++++++ .../hippo/gl/glrenderer/MultiLineTexture.java | 52 + .../hippo/gl/glrenderer/NinePatchChunk.java | 82 + .../hippo/gl/glrenderer/NinePatchTexture.java | 424 +++++ .../com/hippo/gl/glrenderer/RawTexture.java | 73 + .../hippo/gl/glrenderer/ResourceTexture.java | 53 + .../hippo/gl/glrenderer/SpriteTexture.java | 73 + .../hippo/gl/glrenderer/StringTexture.java | 87 + .../com/hippo/gl/glrenderer/TextTexture.java | 181 +++ .../java/com/hippo/gl/glrenderer/Texture.java | 47 + .../hippo/gl/glrenderer/TextureUploader.java | 104 ++ .../com/hippo/gl/glrenderer/TiledTexture.java | 379 +++++ .../hippo/gl/glrenderer/UploadedTexture.java | 296 ++++ .../gl/glrenderer/XmlResourceTexture.java | 79 + .../com/hippo/gl/image/AnimatedImage.java | 122 ++ .../main/java/com/hippo/gl/image/Image.java | 87 + .../java/com/hippo/gl/util/ApiHelper.java | 234 +++ .../java/com/hippo/gl/util/GalleryUtils.java | 56 + .../com/hippo/gl/util/MotionEventHelper.java | 117 ++ .../java/com/hippo/gl/view/AnimationTime.java | 45 + .../main/java/com/hippo/gl/view/GLRoot.java | 55 + .../java/com/hippo/gl/view/GLRootView.java | 807 ++++++++++ .../main/java/com/hippo/gl/view/GLView.java | 1254 +++++++++++++++ .../main/java/com/hippo/gl/view/Gravity.java | 82 + .../com/hippo/gl/view/OrientationSource.java | 22 + .../java/com/hippo/gl/view/TouchHelper.java | 242 +++ .../java/com/hippo/gl/view/TouchOwner.java | 40 + .../java/com/hippo/gl/widget/EdgeEffect.java | 368 +++++ .../java/com/hippo/gl/widget/EdgeView.java | 165 ++ .../java/com/hippo/gl/widget/FrameLayout.java | 101 ++ .../java/com/hippo/gl/widget/GLImageView.java | 114 ++ .../java/com/hippo/gl/widget/ImageView.java | 45 + .../com/hippo/gl/widget/LinearLayout.java | 227 +++ .../com/hippo/gl/widget/ScaledImageView.java | 413 +++++ .../java/com/hippo/gl/widget/TextView.java | 100 ++ .../com/hippo/gl/widget/XmlResourceView.java | 57 + app/src/main/res/layout/activity_gallery.xml | 22 + app/src/main/res/values-v19/themes.xml | 26 + app/src/main/res/values-zh-rCN/strings.xml | 8 +- app/src/main/res/values-zh-rTW/strings.xml | 8 +- app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/themes.xml | 32 +- 67 files changed, 11664 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java create mode 100644 app/src/main/java/com/hippo/ehviewer/ui/gl/DownUpDetector.java create mode 100644 app/src/main/java/com/hippo/ehviewer/ui/gl/GalleryPageView.java create mode 100644 app/src/main/java/com/hippo/ehviewer/ui/gl/GalleryView.java create mode 100644 app/src/main/java/com/hippo/ehviewer/ui/gl/GestureRecognizer.java create mode 100644 app/src/main/java/com/hippo/ehviewer/ui/gl/ImageView.java create mode 100644 app/src/main/java/com/hippo/gl/anim/AlphaAnimation.java create mode 100644 app/src/main/java/com/hippo/gl/anim/Animation.java create mode 100644 app/src/main/java/com/hippo/gl/anim/CanvasAnimation.java create mode 100644 app/src/main/java/com/hippo/gl/anim/FloatAnimation.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/BasicTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/BitmapTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/CanvasTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/ColorTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/ExtTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/FadeInTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/FadeOutTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/FadeTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/GLCanvas.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/GLES20Canvas.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/GLES20IdImpl.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/GLId.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/GLPaint.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/ImageTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/MultiLineTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/NinePatchChunk.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/NinePatchTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/RawTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/ResourceTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/SpriteTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/StringTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/TextTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/Texture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/TextureUploader.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/TiledTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/UploadedTexture.java create mode 100644 app/src/main/java/com/hippo/gl/glrenderer/XmlResourceTexture.java create mode 100644 app/src/main/java/com/hippo/gl/image/AnimatedImage.java create mode 100644 app/src/main/java/com/hippo/gl/image/Image.java create mode 100644 app/src/main/java/com/hippo/gl/util/ApiHelper.java create mode 100644 app/src/main/java/com/hippo/gl/util/GalleryUtils.java create mode 100644 app/src/main/java/com/hippo/gl/util/MotionEventHelper.java create mode 100644 app/src/main/java/com/hippo/gl/view/AnimationTime.java create mode 100644 app/src/main/java/com/hippo/gl/view/GLRoot.java create mode 100644 app/src/main/java/com/hippo/gl/view/GLRootView.java create mode 100644 app/src/main/java/com/hippo/gl/view/GLView.java create mode 100644 app/src/main/java/com/hippo/gl/view/Gravity.java create mode 100644 app/src/main/java/com/hippo/gl/view/OrientationSource.java create mode 100644 app/src/main/java/com/hippo/gl/view/TouchHelper.java create mode 100644 app/src/main/java/com/hippo/gl/view/TouchOwner.java create mode 100644 app/src/main/java/com/hippo/gl/widget/EdgeEffect.java create mode 100644 app/src/main/java/com/hippo/gl/widget/EdgeView.java create mode 100644 app/src/main/java/com/hippo/gl/widget/FrameLayout.java create mode 100644 app/src/main/java/com/hippo/gl/widget/GLImageView.java create mode 100644 app/src/main/java/com/hippo/gl/widget/ImageView.java create mode 100644 app/src/main/java/com/hippo/gl/widget/LinearLayout.java create mode 100644 app/src/main/java/com/hippo/gl/widget/ScaledImageView.java create mode 100644 app/src/main/java/com/hippo/gl/widget/TextView.java create mode 100644 app/src/main/java/com/hippo/gl/widget/XmlResourceView.java create mode 100644 app/src/main/res/layout/activity_gallery.xml create mode 100644 app/src/main/res/values-v19/themes.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 92f6cc9b9..706f62c78 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,6 +22,8 @@ + + + + alpha) x_max = x; + else x_min = x; + } + SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x; + + float y_max = 1.0f; + float y, dy; + while (true) { + y = y_min + (y_max - y_min) / 2.0f; + coef = 3.0f * y * (1.0f - y); + dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y; + if (Math.abs(dy - alpha) < 1E-5) break; + if (dy > alpha) y_max = y; + else y_min = y; + } + SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; + } + SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; + } + + private static final Interpolator FLING_INTERPOLATOR = new Interpolator() { + @Override + public float getInterpolation(float input) { + final int index = (int) (NB_SAMPLES * input); + float distanceCoef = 1.f; + float velocityCoef = 0.f; + if (index < NB_SAMPLES) { + final float t_inf = (float) index / NB_SAMPLES; + final float t_sup = (float) (index + 1) / NB_SAMPLES; + final float d_inf = SPLINE_POSITION[index]; + final float d_sup = SPLINE_POSITION[index + 1]; + velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); + distanceCoef = d_inf + (input - t_inf) * velocityCoef; + } + return distanceCoef; + } + }; + + private static final float[] LEFT_AREA = {0.0f, 0.0f, 1.0f / 3.0f, 1f}; + private static final float[] RIGHT_AREA = {2.0f / 3.0f, 0.0f, 1.0f, 1f}; + private static final float[] CENTER_AREA = {1.0f / 3.0f, 2.0f / 5.0f, 2.0f / 3.0f, 3.0f / 5.0f}; + + private boolean mEnableRequestLayout = true; + + private GestureRecognizer mGestureRecognizer; + + private Adapter mAdapter; + + private int mFirstShownIndex = 0; + private int mLayoutOffset = 0; + + private SparseArray mAttachedPage = new SparseArray<>(); + private List mInvalidAttachedComponent = new ArrayList<>(); + private IntArray mMissedPageInfo = new IntArray(5 * 3); + private List mUsedPage = new ArrayList<>(); + private SparseArray mPageMap = new SparseArray<>(); + + private EdgeView mEdgeView; + + private boolean mScale = false; + private boolean mScroll = false; + private boolean mFirstScroll = false; + private boolean mTouched = false; + + private boolean mCanScrollBetweenPagesHMode; + + private Rect mLeftArea = new Rect(); + private Rect mRightArea = new Rect(); + private Rect mCenterArea = new Rect(); + + private int mDeltaX; + private int mDeltaY; + private int[] mScrollRemain = new int[2]; + + private boolean mHaveChangePage = false; + + @LayoutMode + private int mLayoutMode = LAYOUT_MODE_RIGHT_TO_LEFT; + @LayoutMode + private int mLastLayoutMode = mLayoutMode; + + @Scale + private int mScaleMode = SCALE_FIT; + @StartPosition + private int mStartPosition = START_POSITION_TOP_LEFT; + private float mLastScale = 1f; + + public int mInterval; + + private Fling mFling; + private SmoothScroller mSmoothScroller; + private SmoothScaler mSmoothScaler; + private Recycler mRecycler; + + private boolean mRequestFill = false; + + private ActionListener mListener; + + public GalleryView(Context context, int firstShownIndex, ActionListener listener) { + mFirstShownIndex = firstShownIndex; + mListener = listener; + mEdgeView = new EdgeView(context); + mGestureRecognizer = new GestureRecognizer(context, this); + mSmoothScroller = new SmoothScroller(); + mSmoothScroller.setInterpolator(SMOOTH_SCROLLER_INTERPOLATOR); + mSmoothScaler = new SmoothScaler(); + mSmoothScaler.setInterpolator(SMOOTH_SCALER_INTERPOLATOR); + mFling = new Fling(context); + mFling.setInterpolator(FLING_INTERPOLATOR); + mRecycler = new Recycler(); + mInterval = LayoutUtils.dp2pix(context, INTERVAL_IN_DP); + + setBackgroundColor(BACKGROUND_COLOR); + } + + @Override + public void onAttachToRoot(GLRoot root) { + super.onAttachToRoot(root); + + mEdgeView.onAttachToRoot(root); + } + + @Override + public void onDetachFromRoot() { + super.onDetachFromRoot(); + + mEdgeView.onDetachFromRoot(); + } + + /* + public void setMode(Mode mode) { + if (mMode != mode) { + Mode oldMode = mMode; + mMode = mode; + resetPageMoving(); + superRequestLayout(); + + mListener.onSetMode(mode); + if (oldMode == Mode.NONE) { + mFirstShownIndex = MathUtils.clamp(mFirstShownIndex, 0, mAdapter.getPages() - 1); + mListener.onScrollToPage(mFirstShownIndex, true); + } + } + } + */ + + private void resetPageMoving() { + mSmoothScroller.forceStop(); + mSmoothScaler.forceStop(); + mFling.forceStop(); + mLayoutOffset = 0; + mHaveChangePage = false; + } + + public void setAdapter(Adapter adapter) { + mAdapter = adapter; + } + + public int getPages() { + return mAdapter.getPages(); + } + + public void superRequestLayout() { + super.requestLayout(); + } + + private void requestFill() { + if (GalleryUtils.isRenderThread()) { + fill(); + } else { + mRequestFill = true; + invalidate(); + } + } + + public void scrollToPrevious() { + scrollToPage(mFirstShownIndex - 1); + } + + public void scrollToNext() { + scrollToPage(mFirstShownIndex + 1); + } + + public void scrollToLeft() { + switch (mLastLayoutMode) { + case LAYOUT_MODE_LEFT_TO_RIGHT: + scrollToPrevious(); + break; + case LAYOUT_MODE_RIGHT_TO_LEFT: + scrollToNext(); + break; + case LAYOUT_MODE_TOP_TO_BOTTOM: + // TODO What about top to bottom + break; + } + } + + public void scrollToRight() { + switch (mLastLayoutMode) { + case LAYOUT_MODE_LEFT_TO_RIGHT: + scrollToNext(); + break; + case LAYOUT_MODE_RIGHT_TO_LEFT: + scrollToPrevious(); + break; + case LAYOUT_MODE_TOP_TO_BOTTOM: + // TODO What about top to bottom + break; + } + } + + public void scrollToPage(int page) { + scrollToPageInternal(page, false); + } + + private void scrollToPageInternal(int page, boolean internal) { + if (mAdapter == null || mAdapter.getPages() <= 0) { + return; + } + + if (page < 0) { + switch (mLastLayoutMode) { + case LAYOUT_MODE_LEFT_TO_RIGHT: + mEdgeView.onPull(getWidth() * 2, EdgeView.LEFT); + break; + case LAYOUT_MODE_RIGHT_TO_LEFT: + mEdgeView.onPull(getWidth() * 2, EdgeView.RIGHT); + break; + case LAYOUT_MODE_TOP_TO_BOTTOM: + // TODO What about top to bottom + break; + } + return; + } + + if (page >= mAdapter.getPages()) { + switch (mLastLayoutMode) { + case LAYOUT_MODE_LEFT_TO_RIGHT: + mEdgeView.onPull(getWidth() * 2, EdgeView.RIGHT); + break; + case LAYOUT_MODE_RIGHT_TO_LEFT: + mEdgeView.onPull(getWidth() * 2, EdgeView.LEFT); + break; + case LAYOUT_MODE_TOP_TO_BOTTOM: + // TODO What about top to bottom + break; + } + return; + } + + mSmoothScroller.forceStop(); + mSmoothScaler.forceStop(); + mFling.forceStop(); + + mFirstShownIndex = page; + mLayoutOffset = 0; + requestFill(); + + mListener.onScrollToPage(page, internal); + } + + public void smoothScrollToPage(int page) { + mSmoothScroller.forceStop(); + mSmoothScaler.forceStop(); + mFling.forceStop(); + + // TODO + } + + @Override + protected boolean dispatchTouchEvent(MotionEvent event) { + // Do not pass event to component, so handle event here + mGestureRecognizer.onTouchEvent(event); + return true; + } + + @Override + public boolean onSingleTapUp(float x, float y) { + return true; + } + + @Override + public boolean onSingleTapConfirmed(float x, float y) { + // TODO Let outside decide action + if ((mLayoutMode == LAYOUT_MODE_LEFT_TO_RIGHT || mLayoutMode == LAYOUT_MODE_RIGHT_TO_LEFT) && mLayoutOffset == 0) { + if (mLeftArea.contains((int) x, (int) y)) { + if (mLayoutMode == LAYOUT_MODE_LEFT_TO_RIGHT) { + if (mFirstShownIndex > 0) { + scrollToPageInternal(mFirstShownIndex - 1, true); + } else { + mEdgeView.onPull(getWidth() * 2, (int) y, EdgeView.LEFT); + } + } else { + if (mFirstShownIndex < mAdapter.getPages() - 1) { + scrollToPageInternal(mFirstShownIndex + 1, true); + } else { + mEdgeView.onPull(getWidth() * 2, (int) y, EdgeView.LEFT); + } + } + } else if (mRightArea.contains((int) x, (int) y)) { + if (mLayoutMode == LAYOUT_MODE_LEFT_TO_RIGHT) { + if (mFirstShownIndex < mAdapter.getPages() - 1) { + scrollToPageInternal(mFirstShownIndex + 1, true); + } else { + mEdgeView.onPull(getWidth() * 2, (int) y, EdgeView.RIGHT); + } + } else { + if (mFirstShownIndex > 0) { + scrollToPageInternal(mFirstShownIndex - 1, true); + } else { + mEdgeView.onPull(getWidth() * 2, (int) y, EdgeView.RIGHT); + } + } + } else if (mCenterArea.contains((int) x, (int) y)) { + mListener.onTapCenter(); + } + } + + return true; + } + + @Override + public boolean onDoubleTap(float x, float y) { + return true; + } + + @Override + public boolean onDoubleTapConfirmed(float x, float y) { + if (mScale) { + return false; + } + + // TODO Let outside decide action + if ((mLayoutMode == LAYOUT_MODE_LEFT_TO_RIGHT || mLayoutMode == LAYOUT_MODE_RIGHT_TO_LEFT) && + mLayoutOffset == 0) { + GalleryPageView page = mPageMap.get(mFirstShownIndex); + if (page != null && page.isLoaded()) { + float scale = page.getScale(); + float fitScale = page.getFitScale(); + float centerScale = (1f + fitScale) / 2; + float targetScale; + if ((fitScale > 1f && scale > centerScale) || + (fitScale < 1f && scale < centerScale)) { + targetScale = 1f; + } else { + targetScale = fitScale; + } + mSmoothScaler.startSmoothScaler(page, x, y, scale, targetScale, 500); // TODO + } + } + + return true; + } + + private int doScrollBetweenPagesHMode(int dx) { + boolean reverse = mLastLayoutMode == LAYOUT_MODE_RIGHT_TO_LEFT; + + int remain; + int width = getWidth(); + int newLayoutOffset = mLayoutOffset + dx; + if (!mHaveChangePage) { + if (mLayoutOffset == 0) { + if (dx > mInterval) { + mHaveChangePage = true; + mFirstShownIndex += reverse ? 1 : -1; + mLayoutOffset = -width; + remain = dx - mInterval; + } else if (dx < -mInterval) { + mHaveChangePage = true; + mFirstShownIndex += reverse ? -1 : 1; + mLayoutOffset = width; + remain = dx + mInterval; + } else { + mLayoutOffset = newLayoutOffset; + remain = 0; + } + } else if (mLayoutOffset > 0) { + if (newLayoutOffset > mInterval) { + mHaveChangePage = true; + mFirstShownIndex += reverse ? 1 : -1; + mLayoutOffset = -width; + remain = newLayoutOffset - mInterval; + } else if (newLayoutOffset < 0) { + mLayoutOffset = 0; + remain = newLayoutOffset; + } else { + mLayoutOffset = newLayoutOffset; + remain = 0; + } + } else { // mLayoutOffset < 0 + if (newLayoutOffset < -mInterval) { + mHaveChangePage = true; + mFirstShownIndex += reverse ? -1 : 1; + mLayoutOffset = width; + remain = newLayoutOffset + mInterval; + } else if (mLayoutOffset + dx > 0) { + mLayoutOffset = 0; + remain = newLayoutOffset; + } else { + mLayoutOffset = newLayoutOffset; + remain = 0; + } + } + + // Fix mHaveChangePage + if (mLayoutOffset < mInterval && mLayoutOffset > -mInterval) { + mHaveChangePage = false; + } + } else { + if (mLayoutOffset >= 0) { + if (newLayoutOffset > width) { + mHaveChangePage = false; + mFirstShownIndex += reverse ? 1 : -1; + mLayoutOffset = -mInterval; + remain = newLayoutOffset - width; + } else if (newLayoutOffset < mInterval) { + mHaveChangePage = false; + mLayoutOffset = mInterval; + remain = newLayoutOffset - mInterval; + } else { + mLayoutOffset = newLayoutOffset; + remain = 0; + } + } else { // mLayoutOffset < 0 + if (newLayoutOffset < -width) { + mHaveChangePage = false; + mFirstShownIndex += reverse ? -1 : 1; + mLayoutOffset = mInterval; + remain = newLayoutOffset + width; + } else if (newLayoutOffset > -mInterval) { + mHaveChangePage = false; + mLayoutOffset = -mInterval; + remain = newLayoutOffset + mInterval; + } else { + mLayoutOffset = newLayoutOffset; + remain = 0; + } + } + } + + return remain; + } + + private int scrollBetweenPagesHMode(int dx) { + int remain; + if (mLayoutMode == LAYOUT_MODE_LEFT_TO_RIGHT) { + if (mFirstShownIndex == 0 && mLayoutOffset - dx > 0) { // Start + doScrollBetweenPagesHMode(-mLayoutOffset); + remain = dx - mLayoutOffset; + } else if (mFirstShownIndex == mAdapter.getPages() - 1 && mLayoutOffset - dx < 0) { // end + doScrollBetweenPagesHMode(-mLayoutOffset); + remain = dx - mLayoutOffset; + } else { + remain = -doScrollBetweenPagesHMode(-dx); + } + } else if (mLayoutMode == LAYOUT_MODE_RIGHT_TO_LEFT) { + if (mFirstShownIndex == 0 && mLayoutOffset - dx < 0) { // Start + doScrollBetweenPagesHMode(-mLayoutOffset); + remain = dx - mLayoutOffset; + } else if (mFirstShownIndex == mAdapter.getPages() - 1 && mLayoutOffset - dx > 0) { // end + doScrollBetweenPagesHMode(-mLayoutOffset); + remain = dx - mLayoutOffset; + } else { + remain = -doScrollBetweenPagesHMode(-dx); + } + } else { + throw new IllegalStateException("mLayoutMode can only be " + + "LAYOUT_MODE_LEFT_TO_RIGHT or LAYOUT_MODE_LEFT_TO_RIGHT"); + } + + return remain; + } + + + private void overScrollEdgeForHMode(float x, float y, int dx, int dy) { + if (dx == 0) { + if (mDeltaX != 0) { + mDeltaX = 0; + mEdgeView.onRelease(EdgeView.LEFT); + mEdgeView.onRelease(EdgeView.RIGHT); + } + } else if (dx < 0) { + if (mDeltaX > 0) { + mDeltaX = 0; + mEdgeView.onRelease(EdgeView.LEFT); + mEdgeView.onRelease(EdgeView.RIGHT); + } + mDeltaX += dx; + mEdgeView.onPull(-mDeltaX, y, EdgeView.LEFT); + } else { // dx > 0 + if (mDeltaX < 0) { + mDeltaX = 0; + mEdgeView.onRelease(EdgeView.LEFT); + mEdgeView.onRelease(EdgeView.RIGHT); + } + mDeltaX += dx; + mEdgeView.onPull(mDeltaX, y, EdgeView.RIGHT); + } + + // Only over scroll top bottom edge when image full cover vertically + if (mLayoutOffset != 0) { + return; + } + GalleryPageView page = mPageMap.get(mFirstShownIndex); + if (page == null) { + return; + } + ScaledImageView imageView = page.getImageView(); + if (imageView == null) { + return; + } + ImageTexture imageTexture = imageView.getImageTexture(); + if (imageTexture == null) { + return; + } + if (imageView.getScale() * imageTexture.getHeight() <= getHeight()) { + return; + } + if (dy == 0) { + if (mDeltaY != 0) { + mDeltaY = 0; + mEdgeView.onRelease(EdgeView.TOP); + mEdgeView.onRelease(EdgeView.BOTTOM); + } + } else if (dy < 0) { + if (mDeltaY > 0) { + mDeltaY = 0; + mEdgeView.onRelease(EdgeView.TOP); + mEdgeView.onRelease(EdgeView.BOTTOM); + } + mDeltaY += dy; + mEdgeView.onPull(-mDeltaY, x, EdgeView.TOP); + } else { + if (mDeltaY < 0) { + mDeltaY = 0; + mEdgeView.onRelease(EdgeView.TOP); + mEdgeView.onRelease(EdgeView.BOTTOM); + } + mDeltaY += dy; + mEdgeView.onPull(mDeltaY, x, EdgeView.BOTTOM); + } + } + + @Override + public boolean onScroll(float dx, float dy, float totalX, float totalY, float x, float y) { + if (mScale) { + return false; + } + mScroll = true; + + if (mAdapter == null || mAdapter.getPages() <= 0) { + return true; + } + + if (mFirstScroll) { + mFirstScroll = false; + mCanScrollBetweenPagesHMode = Math.abs(dx) > Math.abs(dy) * 1.5; + } + + int dxI = (int) dx; + int dyI = (int) dy; + switch (mLayoutMode) { + case LAYOUT_MODE_LEFT_TO_RIGHT: + case LAYOUT_MODE_RIGHT_TO_LEFT: { + boolean canImageScroll = true; + boolean needFill = false; + boolean reverse = mLastLayoutMode == LAYOUT_MODE_RIGHT_TO_LEFT; + int remainX = dxI; + int remainY = dyI; + while (remainX != 0 || remainY != 0) { + if (mLayoutOffset != 0 || !canImageScroll) { + if (remainX == 0 || (mFirstShownIndex == 0 && mLayoutOffset == 0 && + (reverse ? -remainX < 0 : -remainX > 0)) || + (mFirstShownIndex == mAdapter.getPages() - 1 && mLayoutOffset == 0 && + (reverse ? -remainX > 0 : -remainX < 0))) { + // On edge + overScrollEdgeForHMode(x, y, remainX, remainY); + remainX = 0; + remainY = 0; + } else if (mCanScrollBetweenPagesHMode) { + remainX = scrollBetweenPagesHMode(remainX); + canImageScroll = true; + needFill = true; + + + } else { + remainX = 0; + remainY = 0; + } + } else { + GalleryPageView page = mPageMap.get(mFirstShownIndex); + if (page != null) { + page.scroll(remainX, remainY, mScrollRemain); + remainX = mScrollRemain[0]; + remainY = mScrollRemain[1]; + } + canImageScroll = false; + } + } + + if (needFill) { + requestFill(); + } + + break; + } + + case LAYOUT_MODE_TOP_TO_BOTTOM: { + // TODO What about top to bottom + break; + } + } + + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + // Only over scroll top bottom edge when image full cover vertically + if (mLayoutOffset != 0) { + return false; + } + GalleryPageView page = mPageMap.get(mFirstShownIndex); + if (page == null) { + return false; + } + ScaledImageView imageView = page.getImageView(); + if (imageView == null) { + return false; + } + ImageTexture imageTexture = imageView.getImageTexture(); + if (imageTexture == null) { + return false; + } + + RectF target = imageView.getTargetRect(); + mFling.startFling(page, (int) velocityX, getWidth() - (int) target.right, (int) -target.left, + (int) velocityY, getHeight() - (int) target.bottom, (int) -target.top); + return true; + } + + @Override + public boolean onScaleBegin(float focusX, float focusY) { + return onScale(focusX, focusY, 1.0f); + } + + private boolean canScale() { + if (mLastLayoutMode != LAYOUT_MODE_LEFT_TO_RIGHT && mLastLayoutMode != LAYOUT_MODE_RIGHT_TO_LEFT) { + return false; + } + if (mLayoutOffset != 0) { + return false; + } + return !mScroll; + } + + @Override + public boolean onScale(float focusX, float focusY, float scale) { + if (!canScale()) { + return false; + } + mScale = true; + + GalleryPageView page = mPageMap.get(mFirstShownIndex); + if (page != null && page.isLoaded()) { + page.scale(focusX, focusY, scale); + mLastScale = page.getScale(); + } + + return true; + } + + @Override + public void onScaleEnd() { + + } + + @Override + public void onDown(float x, float y) { + mSmoothScroller.forceStop(); + mSmoothScaler.forceStop(); + mFling.forceStop(); + mTouched = true; + mScale = false; + mScroll = false; + mFirstScroll = true; + mDeltaX = 0; + mDeltaY = 0; + } + + @Override + public void onUp() { + mTouched = false; + mDeltaX = 0; + mDeltaY = 0; + mEdgeView.onRelease(); + + if ((mLayoutMode == LAYOUT_MODE_LEFT_TO_RIGHT || mLayoutMode == LAYOUT_MODE_RIGHT_TO_LEFT)) { + if (mLayoutOffset != 0) { + // Scroll + final float pageDelta = 7 * (float) Math.abs(mLayoutOffset) / (getWidth() + mInterval); + int duration = (int) ((pageDelta + 1) * 100); + duration = Math.min(duration, MAX_SETTLE_DURATION); + mSmoothScroller.startSmoothScroll(mLayoutOffset, 0, duration); + } else { + // Update AnimateState + GalleryPageView page = mPageMap.get(mFirstShownIndex); + if (page != null) { + page.setAnimateState(ScaledImageView.ANIMATE_STATE_RUN); + } + } + } else if (mLayoutMode == LAYOUT_MODE_TOP_TO_BOTTOM) { + // TODO + } + } + + @Override + public void onPointerDown(float x, float y) { + if (canScale()) { + mScale = true; + } + } + + @Override + public void onPointerUp() { + + } + + @Override + public void requestLayout() { + if (mEnableRequestLayout) { + super.requestLayout(); + } + } + + @Override + protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) { + + mEdgeView.layout(left, top, right, bottom); + + //TODO set all seen UNKNOWN_SEEN in order to setScaleAndOffset + //if (mLastLayoutMode != mMode) { + // mLastLayoutMode = mMode; + // for () { + + // } + //} + + + fill(); + + if (changeSize) { + int width = right - left; + int height = bottom - top; + mLeftArea.set((int) (LEFT_AREA[0] * width), (int) (LEFT_AREA[1] * height), + (int) (LEFT_AREA[2] * width), (int) (LEFT_AREA[3] * height)); + mRightArea.set((int) (RIGHT_AREA[0] * width), (int) (RIGHT_AREA[1] * height), + (int) (RIGHT_AREA[2] * width), (int) (RIGHT_AREA[3] * height)); + mCenterArea.set((int) (CENTER_AREA[0] * width), (int) (CENTER_AREA[1] * height), + (int) (CENTER_AREA[2] * width), (int) (CENTER_AREA[3] * height)); + } + } + + public GalleryPageView getPage(int index) { + return mPageMap.get(index); + } + + private void unbindGalleryPage(GalleryPageView view) { + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.index != NO_POSITION) { + mAdapter.unbindPage(view, lp.index); + } + } + + private void attachePage(GalleryPageView page, int index) { + mPageMap.put(index, page); + } + + // Set lp.index NO_POSITION + private void detachPage(GalleryPageView page) { + LayoutParams lp = (LayoutParams) page.getLayoutParams(); + int index = lp.index; + lp.index = NO_POSITION; + if (index != NO_POSITION) { + mPageMap.remove(index); + } + } + + + private void fillX(int start, int end, int startIndex, boolean reverse) { + for (int i = 0, n = getComponentCount(); i < n; i++) { + GLView component = getComponent(i); + if (component instanceof GalleryPageView) { + mAttachedPage.put(((LayoutParams) component.getLayoutParams()).index, + (GalleryPageView) component); + } else { + mInvalidAttachedComponent.add(component); + } + } + + // Remove invalid view + int n = mInvalidAttachedComponent.size(); + while (n-- > 0) { + removeComponentAt(n); + } + mInvalidAttachedComponent.clear(); + + // Fill + int width = getWidth(); + int height = getHeight(); + int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); + int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + int offset = width + mInterval; + if (reverse) { + offset = -offset; + } + int pages = mAdapter.getPages(); + for (int spec = start, i = startIndex; reverse ? spec > end : spec < end && i < pages; i++) { + GalleryPageView page = mAttachedPage.get(i); + if (page != null) { + mAttachedPage.remove(i); + page.measure(widthSpec, heightSpec); + if (reverse) { + page.layout(spec - width, 0, spec, height); + } else { + page.layout(spec, 0, spec + width, height); + } + } else { + // Add index, left, top, right, bottom + mMissedPageInfo.add(i); + if (reverse) { + mMissedPageInfo.add(spec - width); + mMissedPageInfo.add(0); + mMissedPageInfo.add(spec); + mMissedPageInfo.add(height); + } else { + mMissedPageInfo.add(spec); + mMissedPageInfo.add(0); + mMissedPageInfo.add(spec + width); + mMissedPageInfo.add(height); + } + } + spec += offset; + } + + // Fill missed page + int count = mAttachedPage.size(); + n = mMissedPageInfo.size(); + for (int i = 0; i < n; i += 5) { + int index = mMissedPageInfo.get(i); + GalleryPageView page; + if (i < count) { + page = mAttachedPage.valueAt(i); + // Unbind + unbindGalleryPage(page); + detachPage(page); + mUsedPage.add(page); + } else { + page = mRecycler.obtain(); + addComponent(page); + } + // Bind + ((LayoutParams) page.getLayoutParams()).index = index; + ((LayoutParams) page.getLayoutParams()).seen = PAGE_STATE_UNKNOWN_SEEN; + attachePage(page, index); + // Update index in LayoutParams + mAdapter.bindPage(page, index); + page.measure(widthSpec, heightSpec); + page.layout(mMissedPageInfo.get(i + 1), mMissedPageInfo.get(i + 2), + mMissedPageInfo.get(i + 3), mMissedPageInfo.get(i + 4)); + + } + mMissedPageInfo.clear(); + + // Clear unuse page + for (int i = 0; i < count; i++) { + GalleryPageView page = mAttachedPage.valueAt(i); + if (mUsedPage.contains(page)) { + continue; + } + unbindGalleryPage(page); + detachPage(page); + removeComponent(page); + mRecycler.release(page); + } + mAttachedPage.clear(); + mUsedPage.clear(); + + for (int i = 0; i < getComponentCount(); i++) { + // Update seen + GalleryPageView page = (GalleryPageView) getComponent(i); + LayoutParams lp = ((LayoutParams) page.getLayoutParams()); + int oldSeen = lp.seen; + int seen = page.bounds().intersects(0, 0, width, height) ? PAGE_STATE_SEEN : PAGE_STATE_UNSEEN; + lp.seen = seen; + if (oldSeen == PAGE_STATE_UNKNOWN_SEEN || (oldSeen == PAGE_STATE_SEEN && seen == PAGE_STATE_UNSEEN)) { + page.setScaleOffset(mScaleMode, mStartPosition, mLastScale); + } + + // Update AnimateState + if (seen == PAGE_STATE_UNSEEN) { + page.setAnimateState(ScaledImageView.ANIMATE_STATE_STOP); + } else if (!mTouched && mLayoutOffset == 0 && mFirstShownIndex == lp.index) { + page.setAnimateState(ScaledImageView.ANIMATE_STATE_RUN); + } else { + //page.setAnimateState(ScaledImageView.ANIMATE_STATE_DEFALUE); + } + } + } + + private void fillY(int start, int end, int startIndex, boolean reverse) { + } + + private void fill() { + // Must be in render thread + GalleryUtils.assertInRenderThread(); + + // Disable request layout + mEnableRequestLayout = false; + + if (mAdapter == null) { + // Remove all component and unbind + int i = getComponentCount(); + while (i-- > 0) { + GLView component = getComponent(i); + removeComponentAt(i); + + // Remove from map, unbind gallery page, add to recycler + if (component instanceof GalleryPageView) { + GalleryPageView page = (GalleryPageView) component; + unbindGalleryPage(page); + detachPage(page); + mRecycler.release(page); + } + } + } else if (mAdapter.getPages() == 0) { + // Remove all component and unbind + int i = getComponentCount(); + while (i-- > 0) { + GLView component = getComponent(i); + removeComponentAt(i); + + // Remove from map, unbind gallery page, add to recycler + if (component instanceof GalleryPageView) { + GalleryPageView page = (GalleryPageView) component; + unbindGalleryPage(page); + detachPage(page); + mRecycler.release(page); + } + } + // TODO add + + } else if (mLayoutMode == LAYOUT_MODE_LEFT_TO_RIGHT || mLayoutMode == LAYOUT_MODE_RIGHT_TO_LEFT) { + int width = getWidth(); + int start; + int end; + int startIndex; + boolean reverse = mLayoutMode == LAYOUT_MODE_RIGHT_TO_LEFT; + + if (reverse) { + if (mFirstShownIndex == 0) { + start = width; + startIndex = 0; + } else { + startIndex = mFirstShownIndex - 1; + start = width * 2 + mInterval; + } + if (mFirstShownIndex == mAdapter.getPages() - 1) { + end = 0; + } else { + end = -width - mInterval; + } + } else { + if (mFirstShownIndex == 0) { + start = 0; + startIndex = 0; + } else { + startIndex = mFirstShownIndex - 1; + start = -width - mInterval; + } + if (mFirstShownIndex == mAdapter.getPages() - 1) { + end = width; + } else { + end = width * 2 + mInterval; + } + } + + // Set offset + start += mLayoutOffset; + end += mLayoutOffset; + + fillX(start, end, startIndex, reverse); + } else { // mLayoutMode == LAYOUT_MODE_TOP_TO_BOTTOM + + } + + mEnableRequestLayout = true; + } + + @Override + public void onRender(GLCanvas canvas) { + long time = AnimationTime.get(); + boolean invalidate = mSmoothScroller.calculate(time); + invalidate |= mSmoothScaler.calculate(time); + invalidate |= mFling.calculate(time); + if (invalidate) { + invalidate(); + } + + // mSmoothScroller may do fill too, so check fill request after scroll animator + if (mRequestFill) { + mRequestFill = false; + fill(); + } + + mEdgeView.onRender(canvas); + } + + @Override + protected boolean checkLayoutParams(GLView.LayoutParams p) { + return p instanceof LayoutParams; + } + + @Override + protected GLView.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + + @Override + protected GLView.LayoutParams generateLayoutParams(GLView.LayoutParams p) { + return p == null ? generateDefaultLayoutParams() : new LayoutParams(p); + } + + public static class LayoutParams extends GLView.LayoutParams { + + private int index = NO_POSITION; + @PageState + private int seen = PAGE_STATE_UNKNOWN_SEEN; + + public LayoutParams(GLView.LayoutParams source) { + super(source); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + } + + public static abstract class Adapter { + + public abstract int getPages(); + + public abstract GalleryPageView createPage(); + + public abstract void bindPage(GalleryPageView view, int index); + + public abstract void unbindPage(GalleryPageView view, int index); + + public void notifySizeChange() { + // TODO + } + } + + class SmoothScaler extends Animation { + + private GalleryPageView mPage; + private float mFocusX; + private float mFocusY; + private float mStartScale; + private float mEndScale; + private float mLastScale; + + public void startSmoothScaler(GalleryPageView page, float focusX, float focusY, + float startScale, float endScale, int duration) { + mPage = page; + mFocusX = focusX; + mFocusY = focusY; + mStartScale = startScale; + mEndScale = endScale; + mLastScale = startScale; + setDuration(duration); + start(); + invalidate(); + } + + @Override + protected void onCalculate(float progress) { + float scale = MathUtils.lerp(mStartScale, mEndScale, progress); + mPage.scale(mFocusX, mFocusY, scale / mLastScale); + mLastScale = scale; + if (progress == 1.0f) { + mPage = null; + } + } + } + + class SmoothScroller extends Animation { + + private int mDx; + private int mDy; + private int mLastX; + private int mLastY; + + public void startSmoothScroll(int dx, int dy, int duration) { + mDx = dx; + mDy = dy; + mLastX = 0; + mLastY = 0; + setDuration(duration); + start(); + invalidate(); + } + + @Override + protected void onCalculate(float progress) { + int x = (int) (mDx * progress); + int y = (int) (mDy * progress); + int offsetX = x - mLastX; + while (offsetX != 0) { + int oldOffsetX = offsetX; + offsetX = scrollBetweenPagesHMode(offsetX); + // Avoid loop infinitely + if (offsetX == oldOffsetX) { + break; + } else { + mRequestFill = true; + } + } + mLastX = x; + mLastY = y; + } + } + + class Fling extends Animation { + + private float mPhysicalCoeff; + + private GalleryPageView mPage; + private int mDx; + private int mDy; + private int mLastX; + private int mLastY; + private int[] mTemp = new int[2]; + + public Fling(Context context) { + final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; + mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2) + * 39.37f // inch/meter + * ppi + * 0.84f; // look and feel tuning + } + + private double getSplineDeceleration(int velocity) { + return Math.log(INFLEXION * Math.abs(velocity) / (FLING_FRICTION * mPhysicalCoeff)); + } + + /* Returns the duration, expressed in milliseconds */ + private int getSplineFlingDuration(int velocity) { + final double l = getSplineDeceleration(velocity); + final double decelMinusOne = DECELERATION_RATE - 1.0; + return (int) (1000.0 * Math.exp(l / decelMinusOne)); + } + + private double getSplineFlingDistance(int velocity) { + final double l = getSplineDeceleration(velocity); + final double decelMinusOne = DECELERATION_RATE - 1.0; + return FLING_FRICTION * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); + } + + /** + * Modifies mDuration to the duration it takes to get from start to newFinal using the + * spline interpolation. The previous duration was needed to get to oldFinal. + **/ + private int adjustDuration(int start, int oldFinal, int newFinal, int duration) { + final int oldDistance = oldFinal - start; + final int newDistance = newFinal - start; + final float x = Math.abs((float) newDistance / oldDistance); + final int index = (int) (NB_SAMPLES * x); + if (index < NB_SAMPLES) { + final float x_inf = (float) index / NB_SAMPLES; + final float x_sup = (float) (index + 1) / NB_SAMPLES; + final float t_inf = SPLINE_TIME[index]; + final float t_sup = SPLINE_TIME[index + 1]; + final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf); + duration *= timeCoef; + } + return duration; + } + + public void startFling(GalleryPageView page, int velocityX, int minX, int maxX, + int velocityY, int minY, int maxY) { + mPage = page; + mDx = (int) (getSplineFlingDistance(velocityX) * Math.signum(velocityX)); + mDy = (int) (getSplineFlingDistance(velocityY) * Math.signum(velocityY)); + mLastX = 0; + mLastY = 0; + int durationX = getSplineFlingDuration(velocityX); + int durationY = getSplineFlingDuration(velocityY); + + if (mDx < minX) { + durationX = adjustDuration(0, mDx, minX, durationX); + mDx = minX; + } + if (mDx > maxX) { + durationX = adjustDuration(0, mDx, maxX, durationX); + mDx = maxX; + } + if (mDy < minY) { + durationY = adjustDuration(0, mDy, minY, durationY); + mDy = minY; + } + if (mDy > maxY) { + durationY = adjustDuration(0, mDy, maxY, durationY); + mDy = maxY; + } + + setDuration(Math.max(durationX, durationY)); + start(); + invalidate(); + } + + @Override + protected void onCalculate(float progress) { + int x = (int) (mDx * progress); + int y = (int) (mDy * progress); + int offsetX = x - mLastX; + int offsetY = y - mLastY; + if (offsetX != 0 || offsetY != 0) { + mPage.scroll(-offsetX, -offsetY, mTemp); + } + mLastX = x; + mLastY = y; + } + } + + class Recycler { + + private int mSize = 0; + + private Stack mStack = new Stack<>(); + + private GalleryPageView obtain() { + if (mSize != 0) { + mSize--; + return mStack.pop(); + } else { + return mAdapter.createPage(); + } + } + + public void release(GalleryPageView page) { + if (page == null) { + return; + } + + if (mSize < 3) { // 3 is max size + mSize++; + mStack.push(page); + } + } + } + + public interface ActionListener { + + void onTapCenter(); + + void onScrollToPage(int page, boolean internal); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/gl/GestureRecognizer.java b/app/src/main/java/com/hippo/ehviewer/ui/gl/GestureRecognizer.java new file mode 100644 index 000000000..b58ec462f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/gl/GestureRecognizer.java @@ -0,0 +1,163 @@ +/* + * Copyright 2016 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 + * 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. + */ + +package com.hippo.ehviewer.ui.gl; + +import android.content.Context; +import android.os.SystemClock; +import android.support.v4.view.GestureDetectorCompat; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +// This class aggregates three gesture detectors: GestureDetector, +// ScaleGestureDetector, and DownUpDetector. +public class GestureRecognizer { + @SuppressWarnings("unused") + private static final String TAG = "GestureRecognizer"; + + public interface Listener { + boolean onSingleTapUp(float x, float y); + boolean onSingleTapConfirmed(float x, float y); + boolean onDoubleTap(float x, float y); + boolean onDoubleTapConfirmed(float x, float y); + boolean onScroll(float dx, float dy, float totalX, float totalY, float x, float y); + boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); + boolean onScaleBegin(float focusX, float focusY); + boolean onScale(float focusX, float focusY, float scale); + void onScaleEnd(); + void onDown(float x, float y); + void onUp(); + void onPointerDown(float x, float y); + void onPointerUp(); + } + + private final GestureDetectorCompat mGestureDetector; + private final ScaleGestureDetector mScaleDetector; + private final DownUpDetector mDownUpDetector; + private final Listener mListener; + + public GestureRecognizer(Context context, Listener listener) { + mListener = listener; + MyGestureListener gestureListener = new MyGestureListener(); + mGestureDetector = new GestureDetectorCompat(context, gestureListener, + null /* ignoreMultitouch */); + mGestureDetector.setOnDoubleTapListener(gestureListener); + mScaleDetector = new ScaleGestureDetector( + context, new MyScaleListener()); + mDownUpDetector = new DownUpDetector(new MyDownUpListener()); + } + + public void onTouchEvent(MotionEvent event) { + mScaleDetector.onTouchEvent(event); + mGestureDetector.onTouchEvent(event); + mDownUpDetector.onTouchEvent(event); + } + + public boolean isDown() { + return mDownUpDetector.isDown(); + } + + public void cancelScale() { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mScaleDetector.onTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return mListener.onSingleTapUp(e.getX(), e.getY()); + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + return mListener.onSingleTapConfirmed(e.getX(), e.getY()); + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + if (e.getAction() == MotionEvent.ACTION_UP) { + return mListener.onDoubleTapConfirmed(e.getX(), e.getY()); + } else { + return true; + } + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + return mListener.onDoubleTap(e.getX(), e.getY()); + } + + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + return mListener.onScroll( + dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY(), e2.getX(), e2.getY()); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return mListener.onFling(e1, e2, velocityX, velocityY); + } + } + + private class MyScaleListener + extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return mListener.onScaleBegin( + detector.getFocusX(), detector.getFocusY()); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + return mListener.onScale(detector.getFocusX(), + detector.getFocusY(), detector.getScaleFactor()); + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mListener.onScaleEnd(); + } + } + + private class MyDownUpListener implements DownUpDetector.DownUpListener { + @Override + public void onDown(MotionEvent e) { + mListener.onDown(e.getX(), e.getY()); + } + + @Override + public void onUp(MotionEvent e) { + mListener.onUp(); + } + + @Override + public void onPointerDown(MotionEvent e) { + mListener.onPointerDown(e.getX(), e.getY()); + } + + @Override + public void onPointerUp(MotionEvent e) { + mListener.onPointerUp(); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/gl/ImageView.java b/app/src/main/java/com/hippo/ehviewer/ui/gl/ImageView.java new file mode 100644 index 000000000..ade840b65 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/gl/ImageView.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016 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 + * 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. + */ + +package com.hippo.ehviewer.ui.gl; + +import android.graphics.RectF; + +import com.hippo.gl.glrenderer.GLCanvas; +import com.hippo.gl.glrenderer.Texture; +import com.hippo.gl.view.GLView; + +public class ImageView extends GLView { + + private Texture mTexture; + + private RectF mDst = new RectF(); + + private RectF mSrcActual = new RectF(); + private RectF mDstActual = new RectF(); + + @Override + public void onRender(GLCanvas canvas) { + if (mTexture != null) { + + + + + + mTexture.draw(canvas, mSrcActual, mDstActual); + } + } +} diff --git a/app/src/main/java/com/hippo/gl/anim/AlphaAnimation.java b/app/src/main/java/com/hippo/gl/anim/AlphaAnimation.java new file mode 100644 index 000000000..a033a173b --- /dev/null +++ b/app/src/main/java/com/hippo/gl/anim/AlphaAnimation.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.anim; + +import com.hippo.gl.glrenderer.GLCanvas; +import com.hippo.yorozuya.MathUtils; + +public class AlphaAnimation extends CanvasAnimation { + private final float mStartAlpha; + private final float mEndAlpha; + private float mCurrentAlpha; + + public AlphaAnimation(float from, float to) { + mStartAlpha = from; + mEndAlpha = to; + mCurrentAlpha = from; + } + + @Override + public void apply(GLCanvas canvas) { + canvas.multiplyAlpha(mCurrentAlpha); + } + + @Override + public int getCanvasSaveFlags() { + return GLCanvas.SAVE_FLAG_ALPHA; + } + + @Override + protected void onCalculate(float progress) { + mCurrentAlpha = MathUtils.clamp(mStartAlpha + + (mEndAlpha - mStartAlpha) * progress, 0f, 1f); + } +} diff --git a/app/src/main/java/com/hippo/gl/anim/Animation.java b/app/src/main/java/com/hippo/gl/anim/Animation.java new file mode 100644 index 000000000..eeb9a648e --- /dev/null +++ b/app/src/main/java/com/hippo/gl/anim/Animation.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.anim; + +import android.os.SystemClock; +import android.view.animation.Interpolator; + +import com.hippo.yorozuya.MathUtils; + +// Animation calculates a value according to the current input time. +// +// 1. First we need to use setDuration(int) to set the duration of the +// animation. The duration is in milliseconds. +// 2. Then we should call start(). The actual start time is the first value +// passed to calculate(long). +// 3. Each time we want to get an animation value, we call +// calculate(long currentTimeMillis) to ask the Animation to calculate it. +// The parameter passed to calculate(long) should be nonnegative. +// 4. Use get() to get that value. +// +// In step 3, onCalculate(float progress) is called so subclasses can calculate +// the value according to progress (progress is a value in [0,1]). +// +// Before onCalculate(float) is called, There is an optional interpolator which +// can change the progress value. The interpolator can be set by +// setInterpolator(Interpolator). If the interpolator is used, the value passed +// to onCalculate may be (for example, the overshoot effect). +// +// The isActive() method returns true after the animation start() is called and +// before calculate is passed a value which reaches the duration of the +// animation. +// +// The start() method can be called again to restart the Animation. +// +abstract public class Animation { + private static final long ANIMATION_START = -1; + private static final long NO_ANIMATION = -2; + + /** + * When the animation reaches the end and repeatCount is INFINITE + * or a positive value, the animation restarts from the beginning. + */ + public static final int RESTART = 1; + /** + * When the animation reaches the end and repeatCount is INFINITE + * or a positive value, the animation reverses direction on every iteration. + */ + public static final int REVERSE = 2; + /** + * This value used used with the {@link #setRepeatCount(int)} property to repeat + * the animation indefinitely. + */ + public static final int INFINITE = -1; + + private long mStartTime = NO_ANIMATION; + private long mDuration; + private Interpolator mInterpolator; + private int mRepeatCount; + private int mRepeatMode; + + private int mRunnedCount; + private long mLastFrameTime; + + public void setInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + } + + public void setDuration(long duration) { + mDuration = duration; + } + + public void setRepeatCount(int repeatCount) { + mRepeatCount = repeatCount; + } + + public void setRepeatMode(int repeatMode) { + mRepeatMode = repeatMode; + } + + public boolean isRunning() { + return mStartTime > 0; + } + + public long getLastFrameTime() { + return mLastFrameTime; + } + + public void start() { + if (mStartTime == NO_ANIMATION) { + mStartTime = ANIMATION_START; + mRunnedCount = 0; + mLastFrameTime = 0; + } + } + + public void startAt(long time) { + start(); + setStartTime(time); + } + + public void startNow() { + start(); + setStartTime(SystemClock.uptimeMillis()); + } + + public void setStartTime(long time) { + mStartTime = time; + } + + public boolean isActive() { + return mStartTime != NO_ANIMATION; + } + + public void forceStop() { + mStartTime = NO_ANIMATION; + } + + public void forceReset() { + mStartTime = ANIMATION_START; + mRunnedCount = 0; + mLastFrameTime = 0; + } + + // TODO mRepeatMode + public boolean calculate(long currentTimeMillis) { + if (mStartTime == NO_ANIMATION) { + return false; + } + if (mStartTime == ANIMATION_START) { + mStartTime = currentTimeMillis; + } + mLastFrameTime = currentTimeMillis; + + long elapse = currentTimeMillis - mStartTime; + float x = MathUtils.clamp(mDuration == 0.0f ? 1.0f : (float) elapse / mDuration, 0.0f, 1.0f); // Avoid NaN + Interpolator i = mInterpolator; + onCalculate(i != null ? i.getInterpolation(x) : x); + + if (elapse >= mDuration) { + mRunnedCount++; + if (mRunnedCount >= mRepeatCount && mRepeatCount != INFINITE) { + mStartTime = NO_ANIMATION; + } else { + mStartTime += elapse; + } + } + + return mStartTime != NO_ANIMATION; + } + + abstract protected void onCalculate(float progress); +} diff --git a/app/src/main/java/com/hippo/gl/anim/CanvasAnimation.java b/app/src/main/java/com/hippo/gl/anim/CanvasAnimation.java new file mode 100644 index 000000000..74f1b4570 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/anim/CanvasAnimation.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.anim; + +import com.hippo.gl.glrenderer.GLCanvas; + +public abstract class CanvasAnimation extends Animation { + + public abstract int getCanvasSaveFlags(); + public abstract void apply(GLCanvas canvas); +} diff --git a/app/src/main/java/com/hippo/gl/anim/FloatAnimation.java b/app/src/main/java/com/hippo/gl/anim/FloatAnimation.java new file mode 100644 index 000000000..c2908e09b --- /dev/null +++ b/app/src/main/java/com/hippo/gl/anim/FloatAnimation.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.anim; + +import com.hippo.yorozuya.MathUtils; + +public class FloatAnimation extends Animation { + + private float mFrom; + private float mTo; + private float mCurrent; + + public void setRange(float from, float to) { + mFrom = from; + mTo = to; + } + + @Override + protected void onCalculate(float progress) { + mCurrent = MathUtils.lerp(mFrom, mTo, progress); + } + + public float get() { + return mCurrent; + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/BasicTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/BasicTexture.java new file mode 100644 index 000000000..9cd409df8 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/BasicTexture.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.graphics.RectF; +import android.util.Log; + +import com.hippo.yorozuya.MathUtils; + +import java.util.WeakHashMap; + +// BasicTexture is a Texture corresponds to a real GL texture. +// The state of a BasicTexture indicates whether its data is loaded to GL memory. +// If a BasicTexture is loaded into GL memory, it has a GL texture id. +public abstract class BasicTexture implements Texture { + + @SuppressWarnings("unused") + private static final String TAG = "BasicTexture"; + protected static final int UNSPECIFIED = -1; + + protected static final int STATE_UNLOADED = 0; + protected static final int STATE_LOADED = 1; + protected static final int STATE_ERROR = -1; + + // Log a warning if a texture is larger along a dimension + private static final int MAX_TEXTURE_SIZE = 4096; + + protected int mId = -1; + protected int mState; + + protected int mWidth = UNSPECIFIED; + protected int mHeight = UNSPECIFIED; + + protected int mTextureWidth; + protected int mTextureHeight; + + private boolean mHasBorder; + + protected GLCanvas mCanvasRef = null; + private final static WeakHashMap sAllTextures + = new WeakHashMap<>(); + private static ThreadLocal sInFinalizer = new ThreadLocal<>(); + + protected BasicTexture(GLCanvas canvas, int id, int state) { + setAssociatedCanvas(canvas); + mId = id; + mState = state; + synchronized (sAllTextures) { + sAllTextures.put(this, null); + } + } + + protected BasicTexture() { + this(null, 0, STATE_UNLOADED); + } + + protected void setAssociatedCanvas(GLCanvas canvas) { + mCanvasRef = canvas; + } + + /** + * Sets the content size of this texture. In OpenGL, the actual texture + * size must be of power of 2, the size of the content may be smaller. + */ + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + mTextureWidth = width > 0 ? MathUtils.nextPowerOf2(width) : 0; + mTextureHeight = height > 0 ? MathUtils.nextPowerOf2(height) : 0; + if (mTextureWidth > MAX_TEXTURE_SIZE || mTextureHeight > MAX_TEXTURE_SIZE) { + Log.w(TAG, String.format("texture is too large: %d x %d", + mTextureWidth, mTextureHeight), new Exception()); + } + } + + public boolean isFlippedVertically() { + return false; + } + + public int getId() { + return mId; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + // Returns the width rounded to the next power of 2. + public int getTextureWidth() { + return mTextureWidth; + } + + // Returns the height rounded to the next power of 2. + public int getTextureHeight() { + return mTextureHeight; + } + + // Returns true if the texture has one pixel transparent border around the + // actual content. This is used to avoid jigged edges. + // + // The jigged edges appear because we use GL_CLAMP_TO_EDGE for texture wrap + // mode (GL_CLAMP is not available in OpenGL ES), so a pixel partially + // covered by the texture will use the color of the edge texel. If we add + // the transparent border, the color of the edge texel will be mixed with + // appropriate amount of transparent. + // + // Currently our background is black, so we can draw the thumbnails without + // enabling blending. + public boolean hasBorder() { + return mHasBorder; + } + + protected void setBorder(boolean hasBorder) { + mHasBorder = hasBorder; + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + canvas.drawTexture(this, x, y, getWidth(), getHeight()); + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + canvas.drawTexture(this, x, y, w, h); + } + + @Override + public void draw(GLCanvas canvas, RectF source, RectF target) { + canvas.drawTexture(this, source, target); + } + + // onBind is called before GLCanvas binds this texture. + // It should make sure the data is uploaded to GL memory. + abstract protected boolean onBind(GLCanvas canvas); + + // Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D). + abstract protected int getTarget(); + + public boolean isLoaded() { + return mState == STATE_LOADED; + } + + // recycle() is called when the texture will never be used again, + // so it can free all resources. + public void recycle() { + freeResource(); + } + + // yield() is called when the texture will not be used temporarily, + // so it can free some resources. + // The default implementation unloads the texture from GL memory, so + // the subclass should make sure it can reload the texture to GL memory + // later, or it will have to override this method. + public void yield() { + freeResource(); + } + + private void freeResource() { + GLCanvas canvas = mCanvasRef; + if (canvas != null && mId != -1) { + canvas.unloadTexture(this); + mId = -1; // Don't free it again. + } + mState = STATE_UNLOADED; + setAssociatedCanvas(null); + } + + @Override + protected void finalize() throws Throwable { + try { + sInFinalizer.set(BasicTexture.class); + recycle(); + sInFinalizer.set(null); + } finally { + super.finalize(); + } + } + + // This is for deciding if we can call Bitmap's recycle(). + // We cannot call Bitmap's recycle() in finalizer because at that point + // the finalizer of Bitmap may already be called so recycle() will crash. + public static boolean inFinalizer() { + return sInFinalizer.get() != null; + } + + public static void yieldAllTextures() { + synchronized (sAllTextures) { + for (BasicTexture t : sAllTextures.keySet()) { + t.yield(); + } + } + } + + public static void invalidateAllTextures() { + synchronized (sAllTextures) { + for (BasicTexture t : sAllTextures.keySet()) { + t.mState = STATE_UNLOADED; + t.setAssociatedCanvas(null); + } + } + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/BitmapTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/BitmapTexture.java new file mode 100644 index 000000000..f29270920 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/BitmapTexture.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; + +import com.hippo.yorozuya.AssertUtils; + +// BitmapTexture is a texture whose content is specified by a fixed Bitmap. +// +// The texture does not own the Bitmap. The user should make sure the Bitmap +// is valid during the texture's lifetime. When the texture is recycled, it +// does not free the Bitmap. +public class BitmapTexture extends UploadedTexture { + protected Bitmap mContentBitmap; + + public BitmapTexture(Bitmap bitmap) { + this(bitmap, false); + } + + public BitmapTexture(Bitmap bitmap, boolean hasBorder) { + super(hasBorder); + AssertUtils.assertTrue(bitmap != null && !bitmap.isRecycled()); + mContentBitmap = bitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + // Do nothing. + } + + @Override + protected Bitmap onGetBitmap() { + return mContentBitmap; + } + + public Bitmap getBitmap() { + return mContentBitmap; + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/CanvasTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/CanvasTexture.java new file mode 100644 index 000000000..52e394e8d --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/CanvasTexture.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; + +// CanvasTexture is a texture whose content is the drawing on a Canvas. +// The subclasses should override onDraw() to draw on the bitmap. +// By default CanvasTexture is not opaque. +abstract class CanvasTexture extends UploadedTexture { + private final Config mConfig; + + public CanvasTexture(int width, int height) { + mConfig = Config.ARGB_8888; + setSize(width, height); + setOpaque(false); + } + + @Override + protected Bitmap onGetBitmap() { + Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, mConfig); + Canvas canvas = new Canvas(bitmap); + onDraw(canvas, bitmap); + return bitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + if (!inFinalizer()) { + bitmap.recycle(); + } + } + + abstract protected void onDraw(Canvas canvas, Bitmap backing); +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/ColorTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/ColorTexture.java new file mode 100644 index 000000000..7e926aae1 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/ColorTexture.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.graphics.RectF; + +import com.hippo.yorozuya.ColorUtils; + +// ColorTexture is a texture which fills the rectangle with the specified color. +public class ColorTexture implements Texture { + + private final int mColor; + private int mWidth = 0; + private int mHeight = 0; + + public ColorTexture(int color) { + mColor = color; + } + + private boolean isValidSize() { + return mWidth > 0 && mHeight > 0; + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + if (isValidSize()) { + draw(canvas, x, y, mWidth, mHeight); + } + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + canvas.fillRect(x, y, w, h, mColor); + } + + @Override + public void draw(GLCanvas canvas, RectF source, RectF target) { + canvas.fillRect(target.left, target.top, target.width(), target.height(), mColor); + } + + @Override + public boolean isOpaque() { + return ColorUtils.isOpaque(mColor); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/ExtTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/ExtTexture.java new file mode 100644 index 000000000..9b405bef4 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/ExtTexture.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +// ExtTexture is a texture whose content comes from a external texture. +// Before drawing, setSize() should be called. +public class ExtTexture extends BasicTexture { + + private int mTarget; + + public ExtTexture(GLCanvas canvas, int target) { + GLId glId = canvas.getGLId(); + mId = glId.generateTexture(); + mTarget = target; + } + + private void uploadToCanvas(GLCanvas canvas) { + canvas.setTextureParameters(this); + setAssociatedCanvas(canvas); + mState = STATE_LOADED; + } + + @Override + protected boolean onBind(GLCanvas canvas) { + if (!isLoaded()) { + uploadToCanvas(canvas); + } + + return true; + } + + @Override + public int getTarget() { + return mTarget; + } + + @Override + public boolean isOpaque() { + return true; + } + + @Override + public void yield() { + // we cannot free the texture because we have no backup. + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/FadeInTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/FadeInTexture.java new file mode 100644 index 000000000..5e6435018 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/FadeInTexture.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + + +import android.graphics.RectF; + +// FadeInTexture is a texture which begins with a color, then gradually animates +// into a given texture. +public class FadeInTexture extends FadeTexture implements Texture { + @SuppressWarnings("unused") + private static final String TAG = "FadeInTexture"; + + private final int mColor; + private final TiledTexture mTexture; + + public FadeInTexture(int color, TiledTexture texture) { + super(texture.getWidth(), texture.getHeight(), texture.isOpaque()); + mColor = color; + mTexture = texture; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + if (isAnimating()) { + mTexture.drawMixed(canvas, mColor, getRatio(), x, y, w, h); + } else { + mTexture.draw(canvas, x, y, w, h); + } + } + + @Override + public void draw(GLCanvas canvas, RectF source, RectF target) { + if (isAnimating()) { + mTexture.drawMixed(canvas, mColor, getRatio(), source, target); + } else { + mTexture.draw(canvas, source, target); + } + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/FadeOutTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/FadeOutTexture.java new file mode 100644 index 000000000..ba6f6b609 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/FadeOutTexture.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + + +import android.graphics.RectF; + +// FadeOutTexture is a texture which begins with a given texture, then gradually animates +// into fading out totally. +public class FadeOutTexture extends FadeTexture { + @SuppressWarnings("unused") + private static final String TAG = "FadeOutTexture"; + + private final BasicTexture mTexture; + + public FadeOutTexture(BasicTexture texture) { + super(texture.getWidth(), texture.getHeight(), texture.isOpaque()); + mTexture = texture; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + if (isAnimating()) { + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.setAlpha(getRatio()); + mTexture.draw(canvas, x, y, w, h); + canvas.restore(); + } + } + + @Override + public void draw(GLCanvas canvas, RectF source, RectF target) { + if (isAnimating()) { + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.setAlpha(getRatio()); + mTexture.draw(canvas, source, target); + canvas.restore(); + } + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/FadeTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/FadeTexture.java new file mode 100644 index 000000000..0c543984b --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/FadeTexture.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import com.hippo.gl.view.AnimationTime; +import com.hippo.yorozuya.MathUtils; + +// FadeTexture is a texture which fades the given texture along the time. +public abstract class FadeTexture implements Texture { + @SuppressWarnings("unused") + private static final String TAG = "FadeTexture"; + + // The duration of the fading animation in milliseconds + public static final int DURATION = 180; + + private final long mStartTime; + private final int mWidth; + private final int mHeight; + private final boolean mIsOpaque; + private boolean mIsAnimating; + + public FadeTexture(int width, int height, boolean opaque) { + mWidth = width; + mHeight = height; + mIsOpaque = opaque; + mStartTime = now(); + mIsAnimating = true; + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + draw(canvas, x, y, mWidth, mHeight); + } + + @Override + public boolean isOpaque() { + return mIsOpaque; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + public boolean isAnimating() { + if (mIsAnimating) { + if (now() - mStartTime >= DURATION) { + mIsAnimating = false; + } + } + return mIsAnimating; + } + + protected float getRatio() { + float r = (float)(now() - mStartTime) / DURATION; + return MathUtils.clamp(1.0f - r, 0.0f, 1.0f); + } + + private long now() { + return AnimationTime.get(); + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/GLCanvas.java b/app/src/main/java/com/hippo/gl/glrenderer/GLCanvas.java new file mode 100644 index 000000000..cf17e15c8 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/GLCanvas.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; + +// +// GLCanvas gives a convenient interface to draw using OpenGL. +// +// When a rectangle is specified in this interface, it means the region +// [x, x+width) * [y, y+height) +// +public interface GLCanvas { + + GLId getGLId(); + + // Tells GLCanvas the size of the underlying GL surface. This should be + // called before first drawing and when the size of GL surface is changed. + // This is called by GLRoot and should not be called by the clients + // who only want to draw on the GLCanvas. Both width and height must be + // nonnegative. + void setSize(int width, int height); + + // Clear the drawing buffers. This should only be used by GLRoot. + void clearBuffer(); + + void clearBuffer(float[] argb); + + // Sets and gets the current alpha, alpha must be in [0, 1]. + void setAlpha(float alpha); + + float getAlpha(); + + // (current alpha) = (current alpha) * alpha + void multiplyAlpha(float alpha); + + // Change the current transform matrix. + void translate(float x, float y, float z); + + void translate(float x, float y); + + void scale(float sx, float sy, float sz); + + void rotate(float angle, float x, float y, float z); + + void multiplyMatrix(float[] mMatrix, int offset); + + // Pushes the configuration state (matrix, and alpha) onto + // a private stack. + void save(); + + // Same as save(), but only save those specified in saveFlags. + void save(int saveFlags); + + int SAVE_FLAG_ALL = 0xFFFFFFFF; + int SAVE_FLAG_ALPHA = 0x01; + int SAVE_FLAG_MATRIX = 0x02; + + // Pops from the top of the stack as current configuration state (matrix, + // alpha, and clip). This call balances a previous call to save(), and is + // used to remove all modifications to the configuration state since the + // last save call. + void restore(); + + // Draws a line using the specified paint from (x1, y1) to (x2, y2). + // (Both end points are included). + void drawLine(float x1, float y1, float x2, float y2, GLPaint paint); + + // Draws a rectangle using the specified paint from (x1, y1) to (x2, y2). + // (Both end points are included). + void drawRect(float x1, float y1, float x2, float y2, GLPaint paint); + + // Draws a oval using the specified paint for (cx, cy, radiusX, radiusY) + // (Both end points are included). + void drawOval(float cx, float cy, float radiusX, float radiusY, + GLPaint paint); + + // Draw a arc inside a rect + void drawArc(float cx, float cy, float radiusX, float radiusY, + float sweepAngle, GLPaint paint); + + // Fills the specified rectangle with the specified color. + void fillRect(float x, float y, float width, float height, int color); + + // Fills the specified oval with the specified color. + void fillOval(float cx, float cy, float radiusX, float radiusY, int color); + + // Fills the specified sector with the specified color. + void fillSector(float cx, float cy, float radiusX, float radiusY, float sweepAngle, int color); + + // Draws a texture to the specified rectangle. + void drawTexture(BasicTexture texture, int x, int y, int width, int height); + + void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, int uvBuffer, int indexBuffer, int indexCount); + + // Draws the source rectangle part of the texture to the target rectangle. + void drawTexture(BasicTexture texture, RectF source, RectF target); + + // Draw a texture with a specified texture transform. + void drawTexture(BasicTexture texture, float[] mTextureTransform, int x, int y, int w, int h); + + // Draw two textures to the specified rectangle. The actual texture used is + // from * (1 - ratio) + to * ratio + // The two textures must have the same size. + void drawMixed(BasicTexture from, int toColor, float ratio, int x, int y, int w, int h); + + // Draw a region of a texture and a specified color to the specified + // rectangle. The actual color used is from * (1 - ratio) + to * ratio. + // The region of the texture is defined by parameter "src". The target + // rectangle is specified by parameter "target". + void drawMixed(BasicTexture from, int toColor, float ratio, RectF src, RectF target); + + // Unloads the specified texture from the canvas. The resource allocated + // to draw the texture will be released. The specified texture will return + // to the unloaded state. This function should be called only from + // BasicTexture or its descendant + boolean unloadTexture(BasicTexture texture); + + // Delete the specified buffer object, similar to unloadTexture. + void deleteBuffer(int bufferId); + + // Delete the textures and buffers in GL side. This function should only be + // called in the GL thread. + void deleteRecycledResources(); + + // Dump statistics information and clear the counters. For debug only. + void dumpStatisticsAndClear(); + + void beginRenderTarget(RawTexture texture); + + void endRenderTarget(); + + /** + * Sets texture parameters to use GL_CLAMP_TO_EDGE for both + * GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T. Sets texture parameters to be + * GL_LINEAR for GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER. + * bindTexture() must be called prior to this. + * + * @param texture The texture to set parameters on. + */ + void setTextureParameters(BasicTexture texture); + + /** + * Initializes the texture to a size by calling texImage2D on it. + * + * @param texture The texture to initialize the size. + * @param format The texture format (e.g. GL_RGBA) + * @param type The texture type (e.g. GL_UNSIGNED_BYTE) + */ + void initializeTextureSize(BasicTexture texture, int format, int type); + + /** + * Initializes the texture to a size by calling texImage2D on it. + * + * @param texture The texture to initialize the size. + * @param bitmap The bitmap to initialize the bitmap with. + */ + void initializeTexture(BasicTexture texture, Bitmap bitmap); + + /** + * Calls glTexSubImage2D to upload a bitmap to the texture. + * + * @param texture The target texture to write to. + * @param xOffset Specifies a texel offset in the x direction within the + * texture array. + * @param yOffset Specifies a texel offset in the y direction within the + * texture array. + * @param format The texture format (e.g. GL_RGBA) + * @param type The texture type (e.g. GL_UNSIGNED_BYTE) + */ + void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap, int format, int type); + + /** + * Generates buffers and uploads the buffer data. + * + * @param buffer The buffer to upload + * @return The buffer ID that was generated. + */ + int uploadBuffer(java.nio.FloatBuffer buffer); + + /** + * Generates buffers and uploads the element array buffer data. + * + * @param buffer The buffer to upload + * @return The buffer ID that was generated. + */ + int uploadBuffer(java.nio.ByteBuffer buffer); + + /** + * After LightCycle makes GL calls, this method is called to restore the GL + * configuration to the one expected by GLCanvas. + */ + void recoverFromLightCycle(); + + /** + * Gets the bounds given by x, y, width, and height as well as the internal + * matrix state. There is no special handling for non-90-degree rotations. + * It only considers the lower-left and upper-right corners as the bounds. + * + * @param bounds The output bounds to write to. + * @param x The left side of the input rectangle. + * @param y The bottom of the input rectangle. + * @param width The width of the input rectangle. + * @param height The height of the input rectangle. + */ + void getBounds(Rect bounds, int x, int y, int width, int height); +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/GLES20Canvas.java b/app/src/main/java/com/hippo/gl/glrenderer/GLES20Canvas.java new file mode 100644 index 000000000..61308c61a --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/GLES20Canvas.java @@ -0,0 +1,1072 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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. + */ +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.opengl.Matrix; +import android.util.Log; + +import com.hippo.yorozuya.IntArray; +import com.hippo.yorozuya.MathUtils; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Arrays; + +public class GLES20Canvas implements GLCanvas { + // ************** Constants ********************** + private static final String TAG = GLES20Canvas.class.getSimpleName(); + private static final int FLOAT_SIZE = Float.SIZE / Byte.SIZE; + private static final float OPAQUE_ALPHA = 0.95f; + + private static final int COORDS_PER_VERTEX = 2; + private static final int VERTEX_STRIDE = COORDS_PER_VERTEX * FLOAT_SIZE; + + private static final int COUNT_FILL_VERTEX = 4; + private static final int COUNT_LINE_VERTEX = 2; + private static final int COUNT_RECT_VERTEX = 4; + private static final int COUNT_CIRCLE_VERTEX = 120; // multiple of 4 + private static final int OFFSET_FILL_RECT = 0; + private static final int OFFSET_DRAW_LINE = OFFSET_FILL_RECT + COUNT_FILL_VERTEX; + private static final int OFFSET_DRAW_RECT = OFFSET_DRAW_LINE + COUNT_LINE_VERTEX; + private static final int OFFSET_FILL_CIRCLE = OFFSET_DRAW_RECT + COUNT_RECT_VERTEX; + private static final int OFFSET_DRAW_CIRCLE = OFFSET_FILL_CIRCLE + 1; + private static final int OFFSET_LAST = OFFSET_DRAW_CIRCLE + COUNT_CIRCLE_VERTEX + 1; + + private static final float[] BOX_COORDINATES = new float[OFFSET_LAST * 2]; + + static { + float[] temp = { + 0, 0, // Fill rectangle + 1, 0, + 0, 1, + 1, 1, + 0, 0, // Draw line + 1, 1, + 0, 0, // Draw rectangle outline + 0, 1, + 1, 1, + 1, 0, + 0.5f, 0.5f // Fill circle + }; + System.arraycopy(temp, 0, BOX_COORDINATES, 0, temp.length); + + // Draw circle + int arrayOffset = OFFSET_DRAW_CIRCLE * 2; + for (int i = 0, n = COUNT_CIRCLE_VERTEX / 4; i <= n; i++) { + float value = (float) Math.sin(MathUtils.radians(90f / n * i)) / 2; + float positive = value + 0.5f; + float negative = -value + 0.5f; + BOX_COORDINATES[arrayOffset + n * 0 + i * 2 + 1] = positive; + BOX_COORDINATES[arrayOffset + n * 2 - i * 2 + 0] = positive; + BOX_COORDINATES[arrayOffset + n * 2 + i * 2 + 0] = negative; + BOX_COORDINATES[arrayOffset + n * 4 - i * 2 + 1] = positive; + BOX_COORDINATES[arrayOffset + n * 4 + i * 2 + 1] = negative; + BOX_COORDINATES[arrayOffset + n * 6 - i * 2 + 0] = negative; + BOX_COORDINATES[arrayOffset + n * 6 + i * 2 + 0] = positive; + BOX_COORDINATES[arrayOffset + n * 8 - i * 2 + 1] = negative; + } + } + + private static final float[] BOUNDS_COORDINATES = { + 0, 0, 0, 1, + 1, 1, 0, 1, + }; + + private static final String POSITION_ATTRIBUTE = "aPosition"; + private static final String COLOR_UNIFORM = "uColor"; + private static final String MATRIX_UNIFORM = "uMatrix"; + private static final String TEXTURE_MATRIX_UNIFORM = "uTextureMatrix"; + private static final String TEXTURE_SAMPLER_UNIFORM = "uTextureSampler"; + private static final String ALPHA_UNIFORM = "uAlpha"; + private static final String TEXTURE_COORD_ATTRIBUTE = "aTextureCoordinate"; + + private static final String DRAW_VERTEX_SHADER = "" + + "uniform mat4 " + MATRIX_UNIFORM + ";\n" + + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" + + "void main() {\n" + + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" + + "}\n"; + + private static final String DRAW_FRAGMENT_SHADER = "" + + "precision mediump float;\n" + + "uniform vec4 " + COLOR_UNIFORM + ";\n" + + "void main() {\n" + + " gl_FragColor = " + COLOR_UNIFORM + ";\n" + + "}\n"; + + private static final String TEXTURE_VERTEX_SHADER = "" + + "uniform mat4 " + MATRIX_UNIFORM + ";\n" + + "uniform mat4 " + TEXTURE_MATRIX_UNIFORM + ";\n" + + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" + + " vTextureCoord = (" + TEXTURE_MATRIX_UNIFORM + " * pos).xy;\n" + + "}\n"; + + private static final String MESH_VERTEX_SHADER = "" + + "uniform mat4 " + MATRIX_UNIFORM + ";\n" + + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" + + "attribute vec2 " + TEXTURE_COORD_ATTRIBUTE + ";\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" + + " vTextureCoord = " + TEXTURE_COORD_ATTRIBUTE + ";\n" + + "}\n"; + + private static final String TEXTURE_FRAGMENT_SHADER = "" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform float " + ALPHA_UNIFORM + ";\n" + + "uniform sampler2D " + TEXTURE_SAMPLER_UNIFORM + ";\n" + + "void main() {\n" + + " gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n" + + " gl_FragColor *= " + ALPHA_UNIFORM + ";\n" + + "}\n"; + + private static final String OES_TEXTURE_FRAGMENT_SHADER = "" + + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform float " + ALPHA_UNIFORM + ";\n" + + "uniform samplerExternalOES " + TEXTURE_SAMPLER_UNIFORM + ";\n" + + "void main() {\n" + + " gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n" + + " gl_FragColor *= " + ALPHA_UNIFORM + ";\n" + + "}\n"; + + private static final int INITIAL_RESTORE_STATE_SIZE = 8; + private static final int MATRIX_SIZE = 16; + + // Keep track of restore state + private float[] mMatrices = new float[INITIAL_RESTORE_STATE_SIZE * MATRIX_SIZE]; + private float[] mAlphas = new float[INITIAL_RESTORE_STATE_SIZE]; + private IntArray mSaveFlags = new IntArray(); + + private int mCurrentAlphaIndex = 0; + private int mCurrentMatrixIndex = 0; + + // Viewport size + private int mWidth; + private int mHeight; + + // Projection matrix + private float[] mProjectionMatrix = new float[MATRIX_SIZE]; + + // Screen size for when we aren't bound to a texture + private int mScreenWidth; + private int mScreenHeight; + + // GL programs + private int mDrawProgram; + private int mTextureProgram; + private int mOesTextureProgram; + private int mMeshProgram; + + // GL buffer containing BOX_COORDINATES + private int mBoxCoordinates; + + // Handle indices -- common + private static final int INDEX_POSITION = 0; + private static final int INDEX_MATRIX = 1; + + // Handle indices -- draw + private static final int INDEX_COLOR = 2; + + // Handle indices -- texture + private static final int INDEX_TEXTURE_MATRIX = 2; + private static final int INDEX_TEXTURE_SAMPLER = 3; + private static final int INDEX_ALPHA = 4; + + // Handle indices -- mesh + private static final int INDEX_TEXTURE_COORD = 2; + + private abstract static class ShaderParameter { + public int handle; + protected final String mName; + + public ShaderParameter(String name) { + mName = name; + } + + public abstract void loadHandle(int program); + } + + private static class UniformShaderParameter extends ShaderParameter { + public UniformShaderParameter(String name) { + super(name); + } + + @Override + public void loadHandle(int program) { + handle = GLES20.glGetUniformLocation(program, mName); + checkError(); + } + } + + private static class AttributeShaderParameter extends ShaderParameter { + public AttributeShaderParameter(String name) { + super(name); + } + + @Override + public void loadHandle(int program) { + handle = GLES20.glGetAttribLocation(program, mName); + checkError(); + } + } + + ShaderParameter[] mDrawParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new UniformShaderParameter(COLOR_UNIFORM), // INDEX_COLOR + }; + ShaderParameter[] mTextureParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX + new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER + new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA + }; + ShaderParameter[] mOesTextureParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX + new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER + new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA + }; + ShaderParameter[] mMeshParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new AttributeShaderParameter(TEXTURE_COORD_ATTRIBUTE), // INDEX_TEXTURE_COORD + new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER + new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA + }; + + private final IntArray mUnboundTextures = new IntArray(); + private final IntArray mDeleteBuffers = new IntArray(); + + // Keep track of statistics for debugging + private int mCountDrawMesh = 0; + private int mCountTextureRect = 0; + private int mCountFillRect = 0; + private int mCountDrawLine = 0; + + // Buffer for framebuffer IDs -- we keep track so we can switch the attached + // texture. + private int[] mFrameBuffer = new int[1]; + + // Bound textures. + private ArrayList mTargetTextures = new ArrayList(); + + // Temporary variables used within calculations + private final float[] mTempMatrix = new float[32]; + private final float[] mTempColor = new float[4]; + private final RectF mTempSourceRect = new RectF(); + private final RectF mTempTargetRect = new RectF(); + private final float[] mTempTextureMatrix = new float[MATRIX_SIZE]; + private final int[] mTempIntArray = new int[1]; + + private static final GLId mGLId = new GLES20IdImpl(); + + public GLES20Canvas() { + Matrix.setIdentityM(mTempTextureMatrix, 0); + Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex); + mAlphas[mCurrentAlphaIndex] = 1f; + mTargetTextures.add(null); + + FloatBuffer boxBuffer = createBuffer(BOX_COORDINATES); + mBoxCoordinates = uploadBuffer(boxBuffer); + + int drawVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DRAW_VERTEX_SHADER); + int textureVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, TEXTURE_VERTEX_SHADER); + int meshVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, MESH_VERTEX_SHADER); + int drawFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DRAW_FRAGMENT_SHADER); + int textureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, TEXTURE_FRAGMENT_SHADER); + int oesTextureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, + OES_TEXTURE_FRAGMENT_SHADER); + + mDrawProgram = assembleProgram(drawVertexShader, drawFragmentShader, mDrawParameters); + mTextureProgram = assembleProgram(textureVertexShader, textureFragmentShader, + mTextureParameters); + mOesTextureProgram = assembleProgram(textureVertexShader, oesTextureFragmentShader, + mOesTextureParameters); + mMeshProgram = assembleProgram(meshVertexShader, textureFragmentShader, mMeshParameters); + GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + checkError(); + } + + private static FloatBuffer createBuffer(float[] values) { + // First create an nio buffer, then create a VBO from it. + int size = values.length * FLOAT_SIZE; + FloatBuffer buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + buffer.put(values, 0, values.length).position(0); + return buffer; + } + + private int assembleProgram(int vertexShader, int fragmentShader, ShaderParameter[] params) { + int program = GLES20.glCreateProgram(); + checkError(); + if (program == 0) { + throw new RuntimeException("Cannot create GL program: " + GLES20.glGetError()); + } + GLES20.glAttachShader(program, vertexShader); + checkError(); + GLES20.glAttachShader(program, fragmentShader); + checkError(); + GLES20.glLinkProgram(program); + checkError(); + int[] mLinkStatus = mTempIntArray; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, mLinkStatus, 0); + if (mLinkStatus[0] != GLES20.GL_TRUE) { + Log.e(TAG, "Could not link program: "); + Log.e(TAG, GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + program = 0; + } + for (int i = 0; i < params.length; i++) { + params[i].loadHandle(program); + } + return program; + } + + private static int loadShader(int type, String shaderCode) { + // create a vertex shader type (GLES20.GL_VERTEX_SHADER) + // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) + int shader = GLES20.glCreateShader(type); + + // add the source code to the shader and compile it + GLES20.glShaderSource(shader, shaderCode); + checkError(); + GLES20.glCompileShader(shader); + checkError(); + + return shader; + } + + @Override + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + GLES20.glViewport(0, 0, mWidth, mHeight); + checkError(); + Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex); + Matrix.orthoM(mProjectionMatrix, 0, 0, width, 0, height, -1, 1); + if (getTargetTexture() == null) { + mScreenWidth = width; + mScreenHeight = height; + Matrix.translateM(mMatrices, mCurrentMatrixIndex, 0, height, 0); + Matrix.scaleM(mMatrices, mCurrentMatrixIndex, 1, -1, 1); + } + } + + @Override + public void clearBuffer() { + GLES20.glClearColor(0f, 0f, 0f, 1f); + checkError(); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + checkError(); + } + + @Override + public void clearBuffer(float[] argb) { + GLES20.glClearColor(argb[1], argb[2], argb[3], argb[0]); + checkError(); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + checkError(); + } + + @Override + public float getAlpha() { + return mAlphas[mCurrentAlphaIndex]; + } + + @Override + public void setAlpha(float alpha) { + mAlphas[mCurrentAlphaIndex] = alpha; + } + + @Override + public void multiplyAlpha(float alpha) { + setAlpha(getAlpha() * alpha); + } + + @Override + public void translate(float x, float y, float z) { + Matrix.translateM(mMatrices, mCurrentMatrixIndex, x, y, z); + } + + // This is a faster version of translate(x, y, z) because + // (1) we knows z = 0, (2) we inline the Matrix.translateM call, + // (3) we unroll the loop + @Override + public void translate(float x, float y) { + int index = mCurrentMatrixIndex; + float[] m = mMatrices; + m[index + 12] += m[index + 0] * x + m[index + 4] * y; + m[index + 13] += m[index + 1] * x + m[index + 5] * y; + m[index + 14] += m[index + 2] * x + m[index + 6] * y; + m[index + 15] += m[index + 3] * x + m[index + 7] * y; + } + + @Override + public void scale(float sx, float sy, float sz) { + Matrix.scaleM(mMatrices, mCurrentMatrixIndex, sx, sy, sz); + } + + @Override + public void rotate(float angle, float x, float y, float z) { + if (angle == 0f) { + return; + } + float[] temp = mTempMatrix; + Matrix.setRotateM(temp, 0, angle, x, y, z); + float[] matrix = mMatrices; + int index = mCurrentMatrixIndex; + Matrix.multiplyMM(temp, MATRIX_SIZE, matrix, index, temp, 0); + System.arraycopy(temp, MATRIX_SIZE, matrix, index, MATRIX_SIZE); + } + + @Override + public void multiplyMatrix(float[] matrix, int offset) { + float[] temp = mTempMatrix; + float[] currentMatrix = mMatrices; + int index = mCurrentMatrixIndex; + Matrix.multiplyMM(temp, 0, currentMatrix, index, matrix, offset); + System.arraycopy(temp, 0, currentMatrix, index, 16); + } + + @Override + public void save() { + save(SAVE_FLAG_ALL); + } + + @Override + public void save(int saveFlags) { + boolean saveAlpha = (saveFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA; + if (saveAlpha) { + float currentAlpha = getAlpha(); + mCurrentAlphaIndex++; + if (mAlphas.length <= mCurrentAlphaIndex) { + mAlphas = Arrays.copyOf(mAlphas, mAlphas.length * 2); + } + mAlphas[mCurrentAlphaIndex] = currentAlpha; + } + boolean saveMatrix = (saveFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX; + if (saveMatrix) { + int currentIndex = mCurrentMatrixIndex; + mCurrentMatrixIndex += MATRIX_SIZE; + if (mMatrices.length <= mCurrentMatrixIndex) { + mMatrices = Arrays.copyOf(mMatrices, mMatrices.length * 2); + } + System.arraycopy(mMatrices, currentIndex, mMatrices, mCurrentMatrixIndex, MATRIX_SIZE); + } + mSaveFlags.add(saveFlags); + } + + @Override + public void restore() { + int restoreFlags = mSaveFlags.removeLast(); + boolean restoreAlpha = (restoreFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA; + if (restoreAlpha) { + mCurrentAlphaIndex--; + } + boolean restoreMatrix = (restoreFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX; + if (restoreMatrix) { + mCurrentMatrixIndex -= MATRIX_SIZE; + } + } + + @Override + public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) { + draw(GLES20.GL_LINE_STRIP, OFFSET_DRAW_LINE, COUNT_LINE_VERTEX, x1, y1, x2 - x1, y2 - y1, + paint); + mCountDrawLine++; + } + + @Override + public void drawRect(float x, float y, float width, float height, GLPaint paint) { + draw(GLES20.GL_LINE_LOOP, OFFSET_DRAW_RECT, COUNT_RECT_VERTEX, x, y, width, height, paint); + mCountDrawLine++; + } + + @Override + public void drawOval(float cx, float cy, float radiusX, float radiusY, GLPaint paint) { + float halfLineWidth = paint.getLineWidth() / 2; + fillOval(cx, cy, radiusX + halfLineWidth, radiusY + halfLineWidth, paint.getColor()); + fillOval(cx, cy, radiusX - halfLineWidth, radiusY - halfLineWidth, paint.getBackgroundColor()); + } + + @Override + public void drawArc(float cx, float cy, float radiusX, float radiusY, + float sweepAngle, GLPaint paint) { + float halfLineWidth = paint.getLineWidth() / 2; + fillSector(cx, cy, radiusX + halfLineWidth, radiusY + halfLineWidth, sweepAngle, paint.getColor()); + fillOval(cx, cy, radiusX - halfLineWidth, radiusY - halfLineWidth, paint.getBackgroundColor()); + } + + private void draw(int type, int offset, int count, float x, float y, float width, float height, + GLPaint paint) { + draw(type, offset, count, x, y, width, height, paint.getColor(), paint.getLineWidth()); + } + + private void draw(int type, int offset, int count, float x, float y, float width, float height, + int color, float lineWidth) { + prepareDraw(offset, color, lineWidth); + draw(mDrawParameters, type, count, x, y, width, height); + } + + private void prepareDraw(int offset, int color, float lineWidth) { + GLES20.glUseProgram(mDrawProgram); + checkError(); + if (lineWidth > 0) { + GLES20.glLineWidth(lineWidth); + checkError(); + } + float[] colorArray = getColor(color); + boolean blendingEnabled = (colorArray[3] < 1f); + enableBlending(blendingEnabled); + if (blendingEnabled) { + GLES20.glBlendColor(colorArray[0], colorArray[1], colorArray[2], colorArray[3]); + checkError(); + } + + GLES20.glUniform4fv(mDrawParameters[INDEX_COLOR].handle, 1, colorArray, 0); + setPosition(mDrawParameters, offset); + checkError(); + } + + private float[] getColor(int color) { + float alpha = ((color >>> 24) & 0xFF) / 255f * getAlpha(); + float red = ((color >>> 16) & 0xFF) / 255f * alpha; + float green = ((color >>> 8) & 0xFF) / 255f * alpha; + float blue = (color & 0xFF) / 255f * alpha; + mTempColor[0] = red; + mTempColor[1] = green; + mTempColor[2] = blue; + mTempColor[3] = alpha; + return mTempColor; + } + + private void enableBlending(boolean enableBlending) { + if (enableBlending) { + GLES20.glEnable(GLES20.GL_BLEND); + checkError(); + } else { + GLES20.glDisable(GLES20.GL_BLEND); + checkError(); + } + } + + private void setPosition(ShaderParameter[] params, int offset) { + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mBoxCoordinates); + checkError(); + GLES20.glVertexAttribPointer(params[INDEX_POSITION].handle, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, VERTEX_STRIDE, offset * VERTEX_STRIDE); + checkError(); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + checkError(); + } + + private void draw(ShaderParameter[] params, int type, int count, float x, float y, float width, + float height) { + setMatrix(params, x, y, width, height); + int positionHandle = params[INDEX_POSITION].handle; + GLES20.glEnableVertexAttribArray(positionHandle); + checkError(); + GLES20.glDrawArrays(type, 0, count); + checkError(); + GLES20.glDisableVertexAttribArray(positionHandle); + checkError(); + } + + private void setMatrix(ShaderParameter[] params, float x, float y, float width, float height) { + Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f); + Matrix.scaleM(mTempMatrix, 0, width, height, 1f); + Matrix.multiplyMM(mTempMatrix, MATRIX_SIZE, mProjectionMatrix, 0, mTempMatrix, 0); + GLES20.glUniformMatrix4fv(params[INDEX_MATRIX].handle, 1, false, mTempMatrix, MATRIX_SIZE); + checkError(); + } + + @Override + public void fillRect(float x, float y, float width, float height, int color) { + draw(GLES20.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, COUNT_FILL_VERTEX, x, y, width, height, + color, 0f); + mCountFillRect++; + } + + @Override + public void fillOval(float cx, float cy, float radiusX, float radiusY, int color) { + draw(GLES20.GL_TRIANGLE_FAN, OFFSET_FILL_CIRCLE, OFFSET_LAST - OFFSET_FILL_CIRCLE, + cx - radiusX, cy - radiusY, radiusX * 2, radiusY * 2, color, 0f); + mCountFillRect++; + } + + @Override + public void fillSector(float cx, float cy, float radiusX, float radiusY, float sweepAngle, int color) { + float conjugateAngle = Math.abs(360 - MathUtils.positiveModulo(sweepAngle, 360)); + int conjugateCount = Math.round(COUNT_CIRCLE_VERTEX * conjugateAngle / 360); + if (conjugateCount == 0) { + // It is a circle + fillOval(cx, cy, radiusX, radiusY, color); + } else if (conjugateCount < COUNT_CIRCLE_VERTEX) { + draw(GLES20.GL_TRIANGLE_FAN, OFFSET_FILL_CIRCLE, OFFSET_LAST - OFFSET_FILL_CIRCLE - conjugateCount, + cx - radiusX, cy - radiusY, radiusX * 2, radiusY * 2, color, 0f); + mCountFillRect++; + } + } + + @Override + public void drawTexture(BasicTexture texture, int x, int y, int width, int height) { + if (width <= 0 || height <= 0) { + return; + } + copyTextureCoordinates(texture, mTempSourceRect); + mTempTargetRect.set(x, y, x + width, y + height); + convertCoordinate(mTempSourceRect, mTempTargetRect, texture); + drawTextureRect(texture, mTempSourceRect, mTempTargetRect); + } + + private static void copyTextureCoordinates(BasicTexture texture, RectF outRect) { + int left = 0; + int top = 0; + int right = texture.getWidth(); + int bottom = texture.getHeight(); + if (texture.hasBorder()) { + left = 1; + top = 1; + right -= 1; + bottom -= 1; + } + outRect.set(left, top, right, bottom); + } + + @Override + public void drawTexture(BasicTexture texture, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) { + return; + } + mTempSourceRect.set(source); + mTempTargetRect.set(target); + + convertCoordinate(mTempSourceRect, mTempTargetRect, texture); + drawTextureRect(texture, mTempSourceRect, mTempTargetRect); + } + + @Override + public void drawTexture(BasicTexture texture, float[] textureTransform, int x, int y, int w, + int h) { + if (w <= 0 || h <= 0) { + return; + } + mTempTargetRect.set(x, y, x + w, y + h); + drawTextureRect(texture, textureTransform, mTempTargetRect); + } + + private void drawTextureRect(BasicTexture texture, RectF source, RectF target) { + setTextureMatrix(source); + drawTextureRect(texture, mTempTextureMatrix, target); + } + + private void setTextureMatrix(RectF source) { + mTempTextureMatrix[0] = source.width(); + mTempTextureMatrix[5] = source.height(); + mTempTextureMatrix[12] = source.left; + mTempTextureMatrix[13] = source.top; + } + + // This function changes the source coordinate to the texture coordinates. + // It also clips the source and target coordinates if it is beyond the + // bound of the texture. + private static void convertCoordinate(RectF source, RectF target, BasicTexture texture) { + int width = texture.getWidth(); + int height = texture.getHeight(); + int texWidth = texture.getTextureWidth(); + int texHeight = texture.getTextureHeight(); + // Convert to texture coordinates + source.left /= texWidth; + source.right /= texWidth; + source.top /= texHeight; + source.bottom /= texHeight; + + // Clip if the rendering range is beyond the bound of the texture. + float xBound = (float) width / texWidth; + if (source.right > xBound) { + target.right = target.left + target.width() * (xBound - source.left) / source.width(); + source.right = xBound; + } + float yBound = (float) height / texHeight; + if (source.bottom > yBound) { + target.bottom = target.top + target.height() * (yBound - source.top) / source.height(); + source.bottom = yBound; + } + } + + private void drawTextureRect(BasicTexture texture, float[] textureMatrix, RectF target) { + ShaderParameter[] params = prepareTexture(texture); + setPosition(params, OFFSET_FILL_RECT); + GLES20.glUniformMatrix4fv(params[INDEX_TEXTURE_MATRIX].handle, 1, false, textureMatrix, 0); + checkError(); + if (texture.isFlippedVertically()) { + save(SAVE_FLAG_MATRIX); + translate(0, target.centerY()); + scale(1, -1, 1); + translate(0, -target.centerY()); + } + draw(params, GLES20.GL_TRIANGLE_STRIP, COUNT_FILL_VERTEX, target.left, target.top, + target.width(), target.height()); + if (texture.isFlippedVertically()) { + restore(); + } + mCountTextureRect++; + } + + private ShaderParameter[] prepareTexture(BasicTexture texture) { + ShaderParameter[] params; + int program; + if (texture.getTarget() == GLES20.GL_TEXTURE_2D) { + params = mTextureParameters; + program = mTextureProgram; + } else { + params = mOesTextureParameters; + program = mOesTextureProgram; + } + prepareTexture(texture, program, params); + return params; + } + + private void prepareTexture(BasicTexture texture, int program, ShaderParameter[] params) { + GLES20.glUseProgram(program); + checkError(); + enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + checkError(); + texture.onBind(this); + GLES20.glBindTexture(texture.getTarget(), texture.getId()); + checkError(); + GLES20.glUniform1i(params[INDEX_TEXTURE_SAMPLER].handle, 0); + checkError(); + GLES20.glUniform1f(params[INDEX_ALPHA].handle, getAlpha()); + checkError(); + } + + @Override + public void drawMesh(BasicTexture texture, int x, int y, int xyBuffer, int uvBuffer, + int indexBuffer, int indexCount) { + prepareTexture(texture, mMeshProgram, mMeshParameters); + + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); + checkError(); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, xyBuffer); + checkError(); + int positionHandle = mMeshParameters[INDEX_POSITION].handle; + GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, + VERTEX_STRIDE, 0); + checkError(); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, uvBuffer); + checkError(); + int texCoordHandle = mMeshParameters[INDEX_TEXTURE_COORD].handle; + GLES20.glVertexAttribPointer(texCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, + false, VERTEX_STRIDE, 0); + checkError(); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + checkError(); + + GLES20.glEnableVertexAttribArray(positionHandle); + checkError(); + GLES20.glEnableVertexAttribArray(texCoordHandle); + checkError(); + + setMatrix(mMeshParameters, x, y, 1, 1); + GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, indexCount, GLES20.GL_UNSIGNED_BYTE, 0); + checkError(); + + GLES20.glDisableVertexAttribArray(positionHandle); + checkError(); + GLES20.glDisableVertexAttribArray(texCoordHandle); + checkError(); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + checkError(); + mCountDrawMesh++; + } + + @Override + public void drawMixed(BasicTexture texture, int toColor, float ratio, int x, int y, int w, int h) { + copyTextureCoordinates(texture, mTempSourceRect); + mTempTargetRect.set(x, y, x + w, y + h); + drawMixed(texture, toColor, ratio, mTempSourceRect, mTempTargetRect); + } + + @Override + public void drawMixed(BasicTexture texture, int toColor, float ratio, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) { + return; + } + save(SAVE_FLAG_ALPHA); + + float currentAlpha = getAlpha(); + float cappedRatio = Math.min(1f, Math.max(0f, ratio)); + + float textureAlpha = (1f - cappedRatio) * currentAlpha; + setAlpha(textureAlpha); + drawTexture(texture, source, target); + + float colorAlpha = cappedRatio * currentAlpha; + setAlpha(colorAlpha); + fillRect(target.left, target.top, target.width(), target.height(), toColor); + + restore(); + } + + @Override + public boolean unloadTexture(BasicTexture texture) { + boolean unload = texture.isLoaded(); + if (unload) { + synchronized (mUnboundTextures) { + mUnboundTextures.add(texture.getId()); + } + } + return unload; + } + + @Override + public void deleteBuffer(int bufferId) { + synchronized (mUnboundTextures) { + mDeleteBuffers.add(bufferId); + } + } + + @Override + public void deleteRecycledResources() { + synchronized (mUnboundTextures) { + IntArray ids = mUnboundTextures; + if (mUnboundTextures.size() > 0) { + mGLId.glDeleteTextures(null, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + + ids = mDeleteBuffers; + if (ids.size() > 0) { + mGLId.glDeleteBuffers(null, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + } + } + + @Override + public void dumpStatisticsAndClear() { + String line = String.format("MESH:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", mCountDrawMesh, + mCountTextureRect, mCountFillRect, mCountDrawLine); + mCountDrawMesh = 0; + mCountTextureRect = 0; + mCountFillRect = 0; + mCountDrawLine = 0; + Log.d(TAG, line); + } + + @Override + public void endRenderTarget() { + RawTexture oldTexture = mTargetTextures.remove(mTargetTextures.size() - 1); + RawTexture texture = getTargetTexture(); + setRenderTarget(oldTexture, texture); + restore(); // restore matrix and alpha + } + + @Override + public void beginRenderTarget(RawTexture texture) { + save(); // save matrix and alpha and blending + RawTexture oldTexture = getTargetTexture(); + mTargetTextures.add(texture); + setRenderTarget(oldTexture, texture); + } + + private RawTexture getTargetTexture() { + return mTargetTextures.get(mTargetTextures.size() - 1); + } + + private void setRenderTarget(BasicTexture oldTexture, RawTexture texture) { + if (oldTexture == null && texture != null) { + GLES20.glGenFramebuffers(1, mFrameBuffer, 0); + checkError(); + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer[0]); + checkError(); + } else if (oldTexture != null && texture == null) { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + checkError(); + GLES20.glDeleteFramebuffers(1, mFrameBuffer, 0); + checkError(); + } + + if (texture == null) { + setSize(mScreenWidth, mScreenHeight); + } else { + setSize(texture.getWidth(), texture.getHeight()); + + if (!texture.isLoaded()) { + texture.prepare(this); + } + + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, + texture.getTarget(), texture.getId(), 0); + checkError(); + + checkFramebufferStatus(); + } + } + + private static void checkFramebufferStatus() { + int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); + if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { + String msg = ""; + switch (status) { + case GLES20.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + msg = "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"; + break; + case GLES20.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS: + msg = "GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS"; + break; + case GLES20.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + msg = "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"; + break; + case GLES20.GL_FRAMEBUFFER_UNSUPPORTED: + msg = "GL_FRAMEBUFFER_UNSUPPORTED"; + break; + } + throw new RuntimeException(msg + ":" + Integer.toHexString(status)); + } + } + + @Override + public void setTextureParameters(BasicTexture texture) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + } + + @Override + public void initializeTextureSize(BasicTexture texture, int format, int type) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + int width = texture.getTextureWidth(); + int height = texture.getTextureHeight(); + GLES20.glTexImage2D(target, 0, format, width, height, 0, format, type, null); + } + + @Override + public void initializeTexture(BasicTexture texture, Bitmap bitmap) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + GLUtils.texImage2D(target, 0, bitmap, 0); + } + + @Override + public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap, + int format, int type) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type); + } + + @Override + public int uploadBuffer(FloatBuffer buf) { + return uploadBuffer(buf, FLOAT_SIZE); + } + + @Override + public int uploadBuffer(ByteBuffer buf) { + return uploadBuffer(buf, 1); + } + + private int uploadBuffer(Buffer buffer, int elementSize) { + mGLId.glGenBuffers(1, mTempIntArray, 0); + checkError(); + int bufferId = mTempIntArray[0]; + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferId); + checkError(); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, buffer.capacity() * elementSize, buffer, + GLES20.GL_STATIC_DRAW); + checkError(); + return bufferId; + } + + public static void checkError() { + int error = GLES20.glGetError(); + if (error != 0) { + Throwable t = new Throwable(); + Log.e(TAG, "GL error: " + error, t); + } + } + + @SuppressWarnings("unused") + private static void printMatrix(String message, float[] m, int offset) { + StringBuilder b = new StringBuilder(message); + for (int i = 0; i < MATRIX_SIZE; i++) { + b.append(' '); + if (i % 4 == 0) { + b.append('\n'); + } + b.append(m[offset + i]); + } + Log.v(TAG, b.toString()); + } + + @Override + public void recoverFromLightCycle() { + GLES20.glViewport(0, 0, mWidth, mHeight); + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + checkError(); + } + + @Override + public void getBounds(Rect bounds, int x, int y, int width, int height) { + Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f); + Matrix.scaleM(mTempMatrix, 0, width, height, 1f); + Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE, mTempMatrix, 0, BOUNDS_COORDINATES, 0); + Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE + 4, mTempMatrix, 0, BOUNDS_COORDINATES, 4); + bounds.left = Math.round(mTempMatrix[MATRIX_SIZE]); + bounds.right = Math.round(mTempMatrix[MATRIX_SIZE + 4]); + bounds.top = Math.round(mTempMatrix[MATRIX_SIZE + 1]); + bounds.bottom = Math.round(mTempMatrix[MATRIX_SIZE + 5]); + bounds.sort(); + } + + @Override + public GLId getGLId() { + return mGLId; + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/GLES20IdImpl.java b/app/src/main/java/com/hippo/gl/glrenderer/GLES20IdImpl.java new file mode 100644 index 000000000..27d040dd9 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/GLES20IdImpl.java @@ -0,0 +1,42 @@ +package com.hippo.gl.glrenderer; + +import android.opengl.GLES20; + +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +public class GLES20IdImpl implements GLId { + private final int[] mTempIntArray = new int[1]; + + @Override + public int generateTexture() { + GLES20.glGenTextures(1, mTempIntArray, 0); + GLES20Canvas.checkError(); + return mTempIntArray[0]; + } + + @Override + public void glGenBuffers(int n, int[] buffers, int offset) { + GLES20.glGenBuffers(n, buffers, offset); + GLES20Canvas.checkError(); + } + + @Override + public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) { + GLES20.glDeleteTextures(n, textures, offset); + GLES20Canvas.checkError(); + } + + + @Override + public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) { + GLES20.glDeleteBuffers(n, buffers, offset); + GLES20Canvas.checkError(); + } + + @Override + public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) { + GLES20.glDeleteFramebuffers(n, buffers, offset); + GLES20Canvas.checkError(); + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/GLId.java b/app/src/main/java/com/hippo/gl/glrenderer/GLId.java new file mode 100644 index 000000000..bb06aa32c --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/GLId.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +// This mimics corresponding GL functions. +public interface GLId { + int generateTexture(); + + void glGenBuffers(int n, int[] buffers, int offset); + + void glDeleteTextures(GL11 gl, int n, int[] textures, int offset); + + void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset); + + void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset); +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/GLPaint.java b/app/src/main/java/com/hippo/gl/glrenderer/GLPaint.java new file mode 100644 index 000000000..1e9f37773 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/GLPaint.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import junit.framework.Assert; + +public class GLPaint { + private float mLineWidth = 1f; + private int mColor = 0; + private int mBackgroundColor = 0; + + public void setColor(int color) { + mColor = color; + } + + public int getColor() { + return mColor; + } + + public void setBackgroundColor(int backgroundColor) { + mBackgroundColor = backgroundColor; + } + + public int getBackgroundColor() { + return mBackgroundColor; + } + + public void setLineWidth(float width) { + Assert.assertTrue(width >= 0); + mLineWidth = width; + } + + public float getLineWidth() { + return mLineWidth; + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/ImageTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/ImageTexture.java new file mode 100644 index 000000000..441f00b6d --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/ImageTexture.java @@ -0,0 +1,687 @@ +/* + * 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 + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.RectF; +import android.os.Process; +import android.os.SystemClock; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; + +import com.hippo.gl.image.AnimatedImage; +import com.hippo.gl.image.Image; +import com.hippo.gl.view.GLRoot; +import com.hippo.yorozuya.InfiniteThreadExecutor; +import com.hippo.yorozuya.PVLock; +import com.hippo.yorozuya.PriorityThreadFactory; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.concurrent.LinkedBlockingQueue; + +public class ImageTexture implements Texture { + + @IntDef({SMALL, LARGE}) + @Retention(RetentionPolicy.SOURCE) + private @interface Size {} + + private static final int SMALL = 0; + private static final int LARGE = 1; + + private static final int SMALL_CONTENT_SIZE = 254; + private static final int SMALL_BORDER_SIZE = 1; + private static final int SMALL_TILE_SIZE = SMALL_CONTENT_SIZE + 2 * SMALL_BORDER_SIZE; + + private static final int LARGE_CONTENT_SIZE = SMALL_CONTENT_SIZE * 2; + private static final int LARGE_BORDER_SIZE = SMALL_BORDER_SIZE * 2; + private static final int LARGE_TILE_SIZE = LARGE_CONTENT_SIZE + 2 * LARGE_BORDER_SIZE; + + private static final int INIT_CAPACITY = 8; + + // We are targeting at 60fps, so we have 16ms for each frame. + // In this 16ms, we use about 4~8 ms to upload tiles. + private static final long UPLOAD_TILE_LIMIT = 4; // ms + + private static Bitmap sSmallUploadBitmap; + private static Bitmap sLargeUploadBitmap; + + private static InfiniteThreadExecutor sThreadExecutor; + private static PVLock sPVLock; + + private static Tile sSmallFreeTileHead = null; + private static Tile sLargeFreeTileHead = null; + private static final Object sFreeTileLock = new Object(); + + public static void prepareResources() { + sSmallUploadBitmap = Bitmap.createBitmap(SMALL_TILE_SIZE, SMALL_TILE_SIZE, Bitmap.Config.ARGB_8888); + sLargeUploadBitmap = Bitmap.createBitmap(LARGE_TILE_SIZE, LARGE_TILE_SIZE, Bitmap.Config.ARGB_8888); + sThreadExecutor = new InfiniteThreadExecutor(3000, new LinkedBlockingQueue(), + new PriorityThreadFactory(ImageTexture.class.getSimpleName(), Process.THREAD_PRIORITY_BACKGROUND)); + sPVLock = new PVLock(3); + } + + public static void freeResources() { + sSmallUploadBitmap = null; + sLargeUploadBitmap = null; + sThreadExecutor = null; + sPVLock = null; + } + + private final Image mImage; + private int mUploadIndex = 0; + private final Tile[] mTiles; // Can be modified in different threads. + // Should be protected by "synchronized." + + private final int mWidth; + private final int mHeight; + private final RectF mSrcRect = new RectF(); + private final RectF mDestRect = new RectF(); + + private boolean mRecycled = false; + private boolean mRecycleLock = false; + private boolean mPause = false; + private volatile boolean mConfirmFrame = false; + private int mTargetFrame = -1; + private Callback mCallback; + + public static class Uploader implements GLRoot.OnGLIdleListener { + private final ArrayDeque mTextures = + new ArrayDeque<>(INIT_CAPACITY); + + private final GLRoot mGlRoot; + private boolean mIsQueued = false; + + public Uploader(GLRoot glRoot) { + mGlRoot = glRoot; + } + + public synchronized void clear() { + mTextures.clear(); + } + + public synchronized void addTexture(ImageTexture t) { + if (t.isReady()) return; + mTextures.addLast(t); + + if (mIsQueued) return; + mIsQueued = true; + mGlRoot.addOnGLIdleListener(this); + } + + @Override + public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { + ArrayDeque deque = mTextures; + synchronized (this) { + long now = SystemClock.uptimeMillis(); + long dueTime = now + UPLOAD_TILE_LIMIT; + while (now < dueTime && !deque.isEmpty()) { + ImageTexture t = deque.peekFirst(); + if (t.uploadNextTile(canvas)) { + deque.removeFirst(); + mGlRoot.requestRender(); + } + now = SystemClock.uptimeMillis(); + } + mIsQueued = !mTextures.isEmpty(); + + // return true to keep this listener in the queue + return mIsQueued; + } + } + } + + private static class Tile extends UploadedTexture { + + private int mSize; + public int offsetX; + public int offsetY; + public Image image; + public Tile nextFreeTile; + public int contentWidth; + public int contentHeight; + public int borderSize; + public int tileSize; + public Bitmap uploadBitmap; + + public void setSize(@Size int size, int width, int height) { + mSize = size; + if (size == SMALL) { + borderSize = SMALL_BORDER_SIZE; + tileSize = SMALL_TILE_SIZE; + uploadBitmap = sSmallUploadBitmap; + } else if (size == LARGE) { + borderSize = LARGE_BORDER_SIZE; + tileSize = LARGE_TILE_SIZE; + uploadBitmap = sLargeUploadBitmap; + } else { + throw new IllegalStateException("Not support size " + size); + } + contentWidth = width; + contentHeight = height; + mWidth = width + 2 * borderSize; + mHeight = height + 2 * borderSize; + mTextureWidth = tileSize; + mTextureHeight = tileSize; + } + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + @Override + protected Bitmap onGetBitmap() { + final Image image = this.image; + if (image != null) { + synchronized (image) { + image.copyPixels(offsetX - borderSize, offsetY - borderSize, uploadBitmap, + 0, 0, tileSize, tileSize, Color.TRANSPARENT); + } + } + return uploadBitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + // Nothing + } + + @Override + public boolean isOpaque() { + return false; + } + + private void invalidateInternal() { + invalidateContent(); + image = null; + uploadBitmap = null; + } + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + public void invalidate() { + final Image image = this.image; + if (image != null) { + synchronized (image) { + invalidateInternal(); + } + } else { + invalidateInternal(); + } + } + + public void free() { + switch (mSize) { + case SMALL: + freeSmallTile(this); + break; + case LARGE: + freeLargeTile(this); + break; + default: + throw new IllegalStateException("Not support size " + mSize); + } + } + } + + private static Tile obtainSmallTile() { + synchronized (sFreeTileLock) { + Tile result = sSmallFreeTileHead; + if (result == null) { + return new Tile(); + } else { + sSmallFreeTileHead = result.nextFreeTile; + result.nextFreeTile = null; + } + return result; + } + } + + private static Tile obtainLargeTile() { + synchronized (sFreeTileLock) { + Tile result = sLargeFreeTileHead; + if (result == null) { + return new Tile(); + } else { + sLargeFreeTileHead = result.nextFreeTile; + result.nextFreeTile = null; + } + return result; + } + } + + private static void freeSmallTile(Tile tile) { + tile.invalidate(); + synchronized (sFreeTileLock) { + tile.nextFreeTile = sSmallFreeTileHead; + sSmallFreeTileHead = tile; + } + } + + private static void freeLargeTile(Tile tile) { + tile.invalidate(); + synchronized (sFreeTileLock) { + tile.nextFreeTile = sLargeFreeTileHead; + sLargeFreeTileHead = tile; + } + } + + private boolean uploadNextTile(GLCanvas canvas) { + if (mUploadIndex == mTiles.length) return true; + + synchronized (mTiles) { + Tile next = mTiles[mUploadIndex++]; + + // Make sure tile has not already been recycled by the time + // this is called (race condition in onGLIdle) + if (next.image != null) { + boolean hasBeenLoad = next.isLoaded(); + next.updateContent(canvas); + + // It will take some time for a texture to be drawn for the first + // time. When scrolling, we need to draw several tiles on the screen + // at the same time. It may cause a UI jank even these textures has + // been uploaded. + if (!hasBeenLoad) next.draw(canvas, 0, 0); + } + } + return mUploadIndex == mTiles.length; + } + + private class DecodeTask implements Runnable { + + private boolean checkRecycle() { + if (mRecycled) { + if (!mImage.isRecycled()) { + mImage.recycle(); + } + return true; + } else { + return false; + } + } + + @Override + public void run() { + synchronized (mImage) { + if (checkRecycle()) { + return; + } + mRecycleLock = true; + } + + AnimatedImage aImage = (AnimatedImage) mImage; + + // Avoid a lot of decode thread at same time + sPVLock.p(); + boolean decode = aImage.decode(); + sPVLock.v(); + + synchronized (mImage) { + mRecycleLock = false; + if (checkRecycle()) { + return; + } + } + + // No need to do animate if decode failed + if (!decode) { + return; + } + + long delay; + long time; + int frame = aImage.getCurrentFrame(); + int frameCount = aImage.getFrameCount(); + long forecast = System.currentTimeMillis(); + + // No need to do animate if only on frame + if (frameCount <= 1) { + return; + } + + while (true) { + synchronized (mImage) { + // Check recycle and unlock recycle + mRecycleLock = false; + if (checkRecycle()) { + return; + } + + // Check pause + while (mPause && mTargetFrame == -1) { + try { + mImage.wait(); + } catch (InterruptedException e) { + // Ignore + } + mPause = false; + + // Check recycle + if (checkRecycle()) { + return; + } + + // Update forecast + forecast = System.currentTimeMillis(); + } + + // Get next frame and delay + time = System.currentTimeMillis(); + if (mTargetFrame == -1) { + frame = (frame + 1) % frameCount; + delay = aImage.getDelay(frame) - (time - forecast); + } else { + frame = mTargetFrame; + delay = 0; + mTargetFrame = -1; + } + forecast = time + delay; + } + + // Do sleep + if (delay > 0) { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + // Ignore + } + } + + // Check recycle and lock recycle + synchronized (mImage) { + if (checkRecycle()) { + return; + } + mRecycleLock = true; + } + + aImage.setCurrentFrame(frame); + mConfirmFrame = true; + + // Notify + Callback callback = mCallback; + if (callback != null) { + callback.invalidateImage(mImage); + } + } + } + } + + public ImageTexture(@NonNull Image image) { + mImage = image; + int width = mWidth = image.getWidth(); + int height = mHeight = image.getHeight(); + ArrayList list = new ArrayList<>(); + + for (int x = 0; x < width; x += LARGE_CONTENT_SIZE) { + for (int y = 0; y < height; y += LARGE_CONTENT_SIZE) { + int w = Math.min(LARGE_CONTENT_SIZE, width - x); + int h = Math.min(LARGE_CONTENT_SIZE, height - y); + + if (w <= SMALL_TILE_SIZE) { + Tile tile = obtainSmallTile(); + tile.offsetX = x; + tile.offsetY = y; + tile.image = image; + tile.setSize(SMALL, w, Math.min(SMALL_TILE_SIZE, h)); + list.add(tile); + + int nextHeight = h - SMALL_TILE_SIZE; + if (nextHeight > 0) { + Tile nextTile = obtainSmallTile(); + nextTile.offsetX = x; + nextTile.offsetY = y + SMALL_TILE_SIZE; + nextTile.image = image; + nextTile.setSize(SMALL, w, nextHeight); + list.add(nextTile); + } + + } else if (h <= SMALL_TILE_SIZE) { + Tile tile = obtainSmallTile(); + tile.offsetX = x; + tile.offsetY = y; + tile.image = image; + tile.setSize(SMALL, Math.min(SMALL_TILE_SIZE, w), h); + list.add(tile); + + int nextWidth = w - SMALL_TILE_SIZE; + if (nextWidth > 0) { + Tile nextTile = obtainSmallTile(); + nextTile.offsetX = x + SMALL_TILE_SIZE; + nextTile.offsetY = y; + nextTile.image = image; + nextTile.setSize(SMALL, nextWidth, h); + list.add(nextTile); + } + + } else { + Tile tile = obtainLargeTile(); + tile.offsetX = x; + tile.offsetY = y; + tile.image = image; + tile.setSize(LARGE, w, h); + list.add(tile); + } + } + } + + mTiles = list.toArray(new Tile[list.size()]); + + if (image instanceof AnimatedImage) { + sThreadExecutor.execute(new DecodeTask()); + } + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + // We want to draw the "source" on the "target". + // This method is to find the "output" rectangle which is + // the corresponding area of the "src". + // (x,y) target + // (x0,y0) source +---------------+ + // +----------+ | | + // | src | | output | + // | +--+ | linear map | +----+ | + // | +--+ | ----------> | | | | + // | | by (scaleX, scaleY) | +----+ | + // +----------+ | | + // Texture +---------------+ + // Canvas + private static void mapRect(RectF output, + RectF src, float x0, float y0, float x, float y, float scaleX, + float scaleY) { + output.set(x + (src.left - x0) * scaleX, + y + (src.top - y0) * scaleY, + x + (src.right - x0) * scaleX, + y + (src.bottom - y0) * scaleY); + } + + private void syncFrame() { + if (mConfirmFrame && mImage instanceof AnimatedImage) { + mConfirmFrame = false; + + // invalid tiles + for (int i = 0, n = mTiles.length; i < n; i++) { + Tile tile = mTiles[i]; + int width = tile.mWidth; + int height = tile.mHeight; + tile.invalidateContent(); + tile.mWidth = width; + tile.mHeight = height; + } + } + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + draw(canvas, x, y, mWidth, mHeight); + } + + // Draws the texture on to the specified rectangle. + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float scaleX = (float) w / mWidth; + float scaleY = (float) h / mHeight; + + synchronized (mImage) { + syncFrame(); + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + mapRect(dest, src, 0, 0, x, y, scaleX, scaleY); + src.offset(t.borderSize - t.offsetX, t.borderSize - t.offsetY); + canvas.drawTexture(t, src, dest); + } + } + } + + // Draws a sub region of this texture on to the specified rectangle. + @Override + public void draw(GLCanvas canvas, RectF source, RectF target) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float x0 = source.left; + float y0 = source.top; + float x = target.left; + float y = target.top; + float scaleX = target.width() / source.width(); + float scaleY = target.height() / source.height(); + + synchronized (mImage) { + syncFrame(); + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + if (!src.intersect(source)) + continue; + mapRect(dest, src, x0, y0, x, y, scaleX, scaleY); + src.offset(t.borderSize - t.offsetX, t.borderSize - t.offsetY); + canvas.drawTexture(t, src, dest); + } + } + } + + // Draws a mixed color of this texture and a specified color onto the + // a rectangle. The used color is: from * (1 - ratio) + to * ratio. + public void drawMixed(GLCanvas canvas, int color, float ratio, + int x, int y, int width, int height) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float scaleX = (float) width / mWidth; + float scaleY = (float) height / mHeight; + + synchronized (mImage) { + syncFrame(); + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + mapRect(dest, src, 0, 0, x, y, scaleX, scaleY); + src.offset(t.borderSize - t.offsetX, t.borderSize - t.offsetY); + canvas.drawMixed(t, color, ratio, src, dest); + } + } + } + + public void drawMixed(GLCanvas canvas, int color, float ratio, + RectF source, RectF target) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float x0 = source.left; + float y0 = source.top; + float x = target.left; + float y = target.top; + float scaleX = target.width() / source.width(); + float scaleY = target.height() / source.height(); + + synchronized (mImage) { + syncFrame(); + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + if (!src.intersect(source)) + continue; + mapRect(dest, src, x0, y0, x, y, scaleX, scaleY); + src.offset(t.borderSize - t.offsetX, t.borderSize - t.offsetY); + canvas.drawMixed(t, color, ratio, src, dest); + } + } + } + + @Override + public boolean isOpaque() { + return false; + } + + public boolean isReady() { + return mUploadIndex == mTiles.length; + } + + public void setFrame(int frame) { + synchronized (mImage) { + mTargetFrame = frame; + mImage.notify(); + } + } + + public void setPause(boolean pause) { + synchronized (mImage) { + mPause = pause; + if (!pause) { + mImage.notify(); + } + } + } + + // Can be called in UI thread. + public void recycle() { + synchronized (mImage) { + mRecycled = true; + + for (int i = 0, n = mTiles.length; i < n; ++i) { + mTiles[i].free(); + } + + mImage.notify(); + + if (!mRecycleLock) { + mImage.recycle(); + } + } + } + + public interface Callback { + void invalidateImage(Image image); + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/MultiLineTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/MultiLineTexture.java new file mode 100644 index 000000000..a357401ed --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/MultiLineTexture.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; + + +// MultiLineTexture is a texture shows the content of a specified String. +// +// To create a MultiLineTexture, use the newInstance() method and specify +// the String, the font size, and the color. +class MultiLineTexture extends CanvasTexture { + private final Layout mLayout; + + private MultiLineTexture(Layout layout) { + super(layout.getWidth(), layout.getHeight()); + mLayout = layout; + } + + public static MultiLineTexture newInstance( + String text, int maxWidth, float textSize, int color, + Layout.Alignment alignment) { + TextPaint paint = StringTexture.getDefaultPaint(textSize, color); + Layout layout = new StaticLayout(text, 0, text.length(), paint, + maxWidth, alignment, 1, 0, true, null, 0); + + return new MultiLineTexture(layout); + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + mLayout.draw(canvas); + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/NinePatchChunk.java b/app/src/main/java/com/hippo/gl/glrenderer/NinePatchChunk.java new file mode 100644 index 000000000..d1c5845f9 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/NinePatchChunk.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.graphics.Rect; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// See "frameworks/base/include/utils/ResourceTypes.h" for the format of +// NinePatch chunk. +class NinePatchChunk { + + public static final int NO_COLOR = 0x00000001; + public static final int TRANSPARENT_COLOR = 0x00000000; + + public Rect mPaddings = new Rect(); + + public int mDivX[]; + public int mDivY[]; + public int mColor[]; + + private static void readIntArray(int[] data, ByteBuffer buffer) { + for (int i = 0, n = data.length; i < n; ++i) { + data[i] = buffer.getInt(); + } + } + + private static void checkDivCount(int length) { + if (length == 0 || (length & 0x01) != 0) { + throw new RuntimeException("invalid nine-patch: " + length); + } + } + + public static NinePatchChunk deserialize(byte[] data) { + ByteBuffer byteBuffer = + ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); + + byte wasSerialized = byteBuffer.get(); + if (wasSerialized == 0) return null; + + NinePatchChunk chunk = new NinePatchChunk(); + chunk.mDivX = new int[byteBuffer.get()]; + chunk.mDivY = new int[byteBuffer.get()]; + chunk.mColor = new int[byteBuffer.get()]; + + checkDivCount(chunk.mDivX.length); + checkDivCount(chunk.mDivY.length); + + // skip 8 bytes + byteBuffer.getInt(); + byteBuffer.getInt(); + + chunk.mPaddings.left = byteBuffer.getInt(); + chunk.mPaddings.right = byteBuffer.getInt(); + chunk.mPaddings.top = byteBuffer.getInt(); + chunk.mPaddings.bottom = byteBuffer.getInt(); + + // skip 4 bytes + byteBuffer.getInt(); + + readIntArray(chunk.mDivX, byteBuffer); + readIntArray(chunk.mDivY, byteBuffer); + readIntArray(chunk.mColor, byteBuffer); + + return chunk; + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/NinePatchTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/NinePatchTexture.java new file mode 100644 index 000000000..b616aacf7 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/NinePatchTexture.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; + +import com.hippo.yorozuya.MathUtils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +// NinePatchTexture is a texture backed by a NinePatch resource. +// +// getPaddings() returns paddings specified in the NinePatch. +// getNinePatchChunk() returns the layout data specified in the NinePatch. +// +public class NinePatchTexture extends ResourceTexture { + @SuppressWarnings("unused") + private static final String TAG = "NinePatchTexture"; + private NinePatchChunk mChunk; + private SmallCache mInstanceCache + = new SmallCache(); + + public NinePatchTexture(Context context, int resId) { + super(context, resId); + } + + @Override + protected Bitmap onGetBitmap() { + if (mBitmap != null) return mBitmap; + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bitmap = BitmapFactory.decodeResource( + mContext.getResources(), mResId, options); + mBitmap = bitmap; + setSize(bitmap.getWidth(), bitmap.getHeight()); + byte[] chunkData = bitmap.getNinePatchChunk(); + mChunk = chunkData == null + ? null + : NinePatchChunk.deserialize(bitmap.getNinePatchChunk()); + if (mChunk == null) { + throw new RuntimeException("invalid nine-patch image: " + mResId); + } + return bitmap; + } + + public Rect getPaddings() { + // get the paddings from nine patch + if (mChunk == null) onGetBitmap(); + return mChunk.mPaddings; + } + + public NinePatchChunk getNinePatchChunk() { + if (mChunk == null) onGetBitmap(); + return mChunk; + } + + // This is a simple cache for a small number of things. Linear search + // is used because the cache is small. It also tries to remove less used + // item when the cache is full by moving the often-used items to the front. + private static class SmallCache { + private static final int CACHE_SIZE = 16; + private static final int CACHE_SIZE_START_MOVE = CACHE_SIZE / 2; + private int[] mKey = new int[CACHE_SIZE]; + private V[] mValue = (V[]) new Object[CACHE_SIZE]; + private int mCount; // number of items in this cache + + // Puts a value into the cache. If the cache is full, also returns + // a less used item, otherwise returns null. + public V put(int key, V value) { + if (mCount == CACHE_SIZE) { + V old = mValue[CACHE_SIZE - 1]; // remove the last item + mKey[CACHE_SIZE - 1] = key; + mValue[CACHE_SIZE - 1] = value; + return old; + } else { + mKey[mCount] = key; + mValue[mCount] = value; + mCount++; + return null; + } + } + + public V get(int key) { + for (int i = 0; i < mCount; i++) { + if (mKey[i] == key) { + // Move the accessed item one position to the front, so it + // will less likely to be removed when cache is full. Only + // do this if the cache is starting to get full. + if (mCount > CACHE_SIZE_START_MOVE && i > 0) { + int tmpKey = mKey[i]; + mKey[i] = mKey[i - 1]; + mKey[i - 1] = tmpKey; + + V tmpValue = mValue[i]; + mValue[i] = mValue[i - 1]; + mValue[i - 1] = tmpValue; + } + return mValue[i]; + } + } + return null; + } + + public void clear() { + for (int i = 0; i < mCount; i++) { + mValue[i] = null; // make sure it's can be garbage-collected. + } + mCount = 0; + } + + public int size() { + return mCount; + } + + public V valueAt(int i) { + return mValue[i]; + } + } + + private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) { + int key = w; + key = (key << 16) | h; + NinePatchInstance instance = mInstanceCache.get(key); + + if (instance == null) { + instance = new NinePatchInstance(this, w, h); + NinePatchInstance removed = mInstanceCache.put(key, instance); + if (removed != null) { + removed.recycle(canvas); + } + } + + return instance; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + if (!isLoaded()) { + mInstanceCache.clear(); + } + + if (w != 0 && h != 0) { + findInstance(canvas, w, h).draw(canvas, this, x, y); + } + } + + @Override + public void recycle() { + super.recycle(); + GLCanvas canvas = mCanvasRef; + if (canvas == null) return; + int n = mInstanceCache.size(); + for (int i = 0; i < n; i++) { + NinePatchInstance instance = mInstanceCache.valueAt(i); + instance.recycle(canvas); + } + mInstanceCache.clear(); + } +} + +// This keeps data for a specialization of NinePatchTexture with the size +// (width, height). We pre-compute the coordinates for efficiency. +class NinePatchInstance { + + @SuppressWarnings("unused") + private static final String TAG = "NinePatchInstance"; + + // We need 16 vertices for a normal nine-patch image (the 4x4 vertices) + private static final int VERTEX_BUFFER_SIZE = 16 * 2; + + // We need 22 indices for a normal nine-patch image, plus 2 for each + // transparent region. Current there are at most 1 transparent region. + private static final int INDEX_BUFFER_SIZE = 22 + 2; + + private FloatBuffer mXyBuffer; + private FloatBuffer mUvBuffer; + private ByteBuffer mIndexBuffer; + + // Names for buffer names: xy, uv, index. + private int mXyBufferName = -1; + private int mUvBufferName; + private int mIndexBufferName; + + private int mIdxCount; + + public NinePatchInstance(NinePatchTexture tex, int width, int height) { + NinePatchChunk chunk = tex.getNinePatchChunk(); + + if (width <= 0 || height <= 0) { + throw new RuntimeException("invalid dimension"); + } + + // The code should be easily extended to handle the general cases by + // allocating more space for buffers. But let's just handle the only + // use case. + if (chunk.mDivX.length != 2 || chunk.mDivY.length != 2) { + throw new RuntimeException("unsupported nine patch"); + } + + float divX[] = new float[4]; + float divY[] = new float[4]; + float divU[] = new float[4]; + float divV[] = new float[4]; + + int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width); + int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height); + + prepareVertexData(divX, divY, divU, divV, nx, ny, chunk.mColor); + } + + /** + * Stretches the texture according to the nine-patch rules. It will + * linearly distribute the strechy parts defined in the nine-patch chunk to + * the target area. + * + *
+     *                      source
+     *          /--------------^---------------\
+     *         u0    u1       u2  u3     u4   u5
+     * div ---> |fffff|ssssssss|fff|ssssss|ffff| ---> u
+     *          |    div0    div1 div2   div3  |
+     *          |     |       /   /      /    /
+     *          |     |      /   /     /    /
+     *          |     |     /   /    /    /
+     *          |fffff|ssss|fff|sss|ffff| ---> x
+     *         x0    x1   x2  x3  x4   x5
+     *          \----------v------------/
+     *                  target
+     *
+     * f: fixed segment
+     * s: stretchy segment
+     * 
+ * + * @param div the stretch parts defined in nine-patch chunk + * @param source the length of the texture + * @param target the length on the drawing plan + * @param u output, the positions of these dividers in the texture + * coordinate + * @param x output, the corresponding position of these dividers on the + * drawing plan + * @return the number of these dividers. + */ + private static int stretch( + float x[], float u[], int div[], int source, int target) { + int textureSize = MathUtils.nextPowerOf2(source); + float textureBound = (float) source / textureSize; + + float stretch = 0; + for (int i = 0, n = div.length; i < n; i += 2) { + stretch += div[i + 1] - div[i]; + } + + float remaining = target - source + stretch; + + float lastX = 0; + float lastU = 0; + + x[0] = 0; + u[0] = 0; + for (int i = 0, n = div.length; i < n; i += 2) { + // Make the stretchy segment a little smaller to prevent sampling + // on neighboring fixed segments. + // fixed segment + x[i + 1] = lastX + (div[i] - lastU) + 0.5f; + u[i + 1] = Math.min((div[i] + 0.5f) / textureSize, textureBound); + + // stretchy segment + float partU = div[i + 1] - div[i]; + float partX = remaining * partU / stretch; + remaining -= partX; + stretch -= partU; + + lastX = x[i + 1] + partX; + lastU = div[i + 1]; + x[i + 2] = lastX - 0.5f; + u[i + 2] = Math.min((lastU - 0.5f)/ textureSize, textureBound); + } + // the last fixed segment + x[div.length + 1] = target; + u[div.length + 1] = textureBound; + + // remove segments with length 0. + int last = 0; + for (int i = 1, n = div.length + 2; i < n; ++i) { + if ((x[i] - x[last]) < 1f) continue; + x[++last] = x[i]; + u[last] = u[i]; + } + return last + 1; + } + + private void prepareVertexData(float x[], float y[], float u[], float v[], + int nx, int ny, int[] color) { + /* + * Given a 3x3 nine-patch image, the vertex order is defined as the + * following graph: + * + * (0) (1) (2) (3) + * | /| /| /| + * | / | / | / | + * (4) (5) (6) (7) + * | \ | \ | \ | + * | \| \| \| + * (8) (9) (A) (B) + * | /| /| /| + * | / | / | / | + * (C) (D) (E) (F) + * + * And we draw the triangle strip in the following index order: + * + * index: 04152637B6A5948C9DAEBF + */ + int pntCount = 0; + float xy[] = new float[VERTEX_BUFFER_SIZE]; + float uv[] = new float[VERTEX_BUFFER_SIZE]; + for (int j = 0; j < ny; ++j) { + for (int i = 0; i < nx; ++i) { + int xIndex = (pntCount++) << 1; + int yIndex = xIndex + 1; + xy[xIndex] = x[i]; + xy[yIndex] = y[j]; + uv[xIndex] = u[i]; + uv[yIndex] = v[j]; + } + } + + int idxCount = 1; + boolean isForward = false; + byte index[] = new byte[INDEX_BUFFER_SIZE]; + for (int row = 0; row < ny - 1; row++) { + --idxCount; + isForward = !isForward; + + int start, end, inc; + if (isForward) { + start = 0; + end = nx; + inc = 1; + } else { + start = nx - 1; + end = -1; + inc = -1; + } + + for (int col = start; col != end; col += inc) { + int k = row * nx + col; + if (col != start) { + int colorIdx = row * (nx - 1) + col; + if (isForward) colorIdx--; + if (color[colorIdx] == NinePatchChunk.TRANSPARENT_COLOR) { + index[idxCount] = index[idxCount - 1]; + ++idxCount; + index[idxCount++] = (byte) k; + } + } + + index[idxCount++] = (byte) k; + index[idxCount++] = (byte) (k + nx); + } + } + + mIdxCount = idxCount; + + int size = (pntCount * 2) * (Float.SIZE / Byte.SIZE); + mXyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + mUvBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + mIndexBuffer = allocateDirectNativeOrderBuffer(mIdxCount); + + mXyBuffer.put(xy, 0, pntCount * 2).position(0); + mUvBuffer.put(uv, 0, pntCount * 2).position(0); + mIndexBuffer.put(index, 0, idxCount).position(0); + } + + private static ByteBuffer allocateDirectNativeOrderBuffer(int size) { + return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); + } + + private void prepareBuffers(GLCanvas canvas) { + mXyBufferName = canvas.uploadBuffer(mXyBuffer); + mUvBufferName = canvas.uploadBuffer(mUvBuffer); + mIndexBufferName = canvas.uploadBuffer(mIndexBuffer); + + // These buffers are never used again. + mXyBuffer = null; + mUvBuffer = null; + mIndexBuffer = null; + } + + public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) { + if (mXyBufferName == -1) { + prepareBuffers(canvas); + } + canvas.drawMesh(tex, x, y, mXyBufferName, mUvBufferName, mIndexBufferName, mIdxCount); + } + + public void recycle(GLCanvas canvas) { + if (mXyBuffer == null) { + canvas.deleteBuffer(mXyBufferName); + canvas.deleteBuffer(mUvBufferName); + canvas.deleteBuffer(mIndexBufferName); + mXyBufferName = -1; + } + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/RawTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/RawTexture.java new file mode 100644 index 000000000..a58c6cc6d --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/RawTexture.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.util.Log; + +import javax.microedition.khronos.opengles.GL11; + +public class RawTexture extends BasicTexture { + private static final String TAG = "RawTexture"; + + private final boolean mOpaque; + private boolean mIsFlipped; + + public RawTexture(int width, int height, boolean opaque) { + mOpaque = opaque; + setSize(width, height); + } + + @Override + public boolean isOpaque() { + return mOpaque; + } + + @Override + public boolean isFlippedVertically() { + return mIsFlipped; + } + + public void setIsFlippedVertically(boolean isFlipped) { + mIsFlipped = isFlipped; + } + + protected void prepare(GLCanvas canvas) { + GLId glId = canvas.getGLId(); + mId = glId.generateTexture(); + canvas.initializeTextureSize(this, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE); + canvas.setTextureParameters(this); + mState = STATE_LOADED; + setAssociatedCanvas(canvas); + } + + @Override + protected boolean onBind(GLCanvas canvas) { + if (isLoaded()) return true; + Log.w(TAG, "lost the content due to context change"); + return false; + } + + @Override + public void yield() { + // we cannot free the texture because we have no backup. + } + + @Override + protected int getTarget() { + return GL11.GL_TEXTURE_2D; + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/ResourceTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/ResourceTexture.java new file mode 100644 index 000000000..3aaf1e317 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/ResourceTexture.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import junit.framework.Assert; + +// ResourceTexture is a texture whose Bitmap is decoded from a resource. +// By default ResourceTexture is not opaque. +public class ResourceTexture extends UploadedTexture { + + protected final Context mContext; + protected final int mResId; + + public ResourceTexture(Context context, int resId) { + Assert.assertNotNull(context); + mContext = context; + mResId = resId; + setOpaque(false); + } + + @Override + protected Bitmap onGetBitmap() { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeResource( + mContext.getResources(), mResId, options); + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + if (!inFinalizer()) { + bitmap.recycle(); + } + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/SpriteTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/SpriteTexture.java new file mode 100644 index 000000000..fcfeac8ec --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/SpriteTexture.java @@ -0,0 +1,73 @@ +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.RectF; + +import com.hippo.yorozuya.AssertUtils; + +public class SpriteTexture extends TiledTexture { + + private int mCount; + private int[] mRects; + + private RectF mTempSource = new RectF(); + private RectF mTempTarget = new RectF(); + + public SpriteTexture(Bitmap bitmap, int count, int[] rects) { + super(bitmap); + + AssertUtils.assertEquals("rects.length must be count * 4", count * 4, rects.length); + mCount = count; + mRects = rects; + } + + public int getCount() { + return mCount; + } + + public void drawSprite(GLCanvas canvas, int index, int x, int y) { + int[] rects = mRects; + int offset = index * 4; + int sourceX = rects[offset]; + int sourceY = rects[offset + 1]; + int sourceWidth = rects[offset + 2]; + int sourceHeight = rects[offset + 3]; + mTempSource.set(sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight); + mTempTarget.set(x, y, x + sourceWidth, y + sourceHeight); + draw(canvas, mTempSource, mTempTarget); + } + + public void drawSprite(GLCanvas canvas, int index, int x, int y, int width, int height) { + int[] rects = mRects; + int offset = index * 4; + int sourceX = rects[offset]; + int sourceY = rects[offset + 1]; + mTempSource.set(sourceX, sourceY, sourceX + rects[offset + 2], sourceY + rects[offset + 3]); + mTempTarget.set(x, y, x + width, y + height); + draw(canvas, mTempSource, mTempTarget); + } + + public void drawSpriteMixed(GLCanvas canvas, int index, int color, float ratio, int x, int y) { + int[] rects = mRects; + int offset = index * 4; + int sourceX = rects[offset]; + int sourceY = rects[offset + 1]; + int sourceWidth = rects[offset + 2]; + int sourceHeight = rects[offset + 3]; + mTempSource.set(sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight); + mTempTarget.set(x, y, x + sourceWidth, y + sourceHeight); + drawMixed(canvas, color, ratio, mTempSource, mTempTarget); + } + + public void drawSpriteMixed(GLCanvas canvas, int index, int color, float ratio, + int x, int y, int width, int height) { + int[] rects = mRects; + int offset = index * 4; + int sourceX = rects[offset]; + int sourceY = rects[offset + 1]; + mTempSource.set(sourceX, sourceY, sourceX + rects[offset + 2], + sourceY + rects[offset + 3]); + mTempTarget.set(x, y, x + width, y + height); + drawMixed(canvas, color, ratio, mTempSource, mTempTarget); + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/StringTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/StringTexture.java new file mode 100644 index 000000000..609e79509 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/StringTexture.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.text.TextUtils; + +// StringTexture is a texture shows the content of a specified String. +// +// To create a StringTexture, use the newInstance() method and specify +// the String, the font size, and the color. +public class StringTexture extends CanvasTexture { + private final String mText; + private final TextPaint mPaint; + private final FontMetricsInt mMetrics; + + private StringTexture(String text, TextPaint paint, + FontMetricsInt metrics, int width, int height) { + super(width, height); + mText = text; + mPaint = paint; + mMetrics = metrics; + } + + public static TextPaint getDefaultPaint(float textSize, int color) { + TextPaint paint = new TextPaint(); + paint.setTextSize(textSize); + paint.setAntiAlias(true); + paint.setColor(color); + paint.setShadowLayer(2f, 0f, 0f, Color.BLACK); + return paint; + } + + public static StringTexture newInstance( + String text, float textSize, int color) { + return newInstance(text, getDefaultPaint(textSize, color)); + } + + public static StringTexture newInstance( + String text, float textSize, int color, + float lengthLimit, boolean isBold) { + TextPaint paint = getDefaultPaint(textSize, color); + if (isBold) { + paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); + } + if (lengthLimit > 0) { + text = TextUtils.ellipsize( + text, paint, lengthLimit, TextUtils.TruncateAt.END).toString(); + } + return newInstance(text, paint); + } + + private static StringTexture newInstance(String text, TextPaint paint) { + FontMetricsInt metrics = paint.getFontMetricsInt(); + int width = (int) Math.ceil(paint.measureText(text)); + int height = metrics.bottom - metrics.top; + // The texture size needs to be at least 1x1. + if (width <= 0) width = 1; + if (height <= 0) height = 1; + return new StringTexture(text, paint, metrics, width, height); + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + canvas.translate(0, -mMetrics.ascent); + canvas.drawText(mText, 0, 0, mPaint); + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/TextTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/TextTexture.java new file mode 100644 index 000000000..22b6848ad --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/TextTexture.java @@ -0,0 +1,181 @@ +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Typeface; + +import java.util.Arrays; + +// TODO support multiline +/** + * Only support single line + */ +public class TextTexture extends SpriteTexture { + + private char[] mCharacters; + private float[] mWidths; + private float mHeight; + private float mMaxWidth; + + private TextTexture(Bitmap bitmap, int count, int[] rects, char[] characters, + float[] widths, float height, float maxWidth) { + super(bitmap, count, rects); + mCharacters = characters; + mWidths = widths; + mHeight = height; + mMaxWidth = maxWidth; + } + + public int[] getTextIndexes(String text) { + char[] characters = mCharacters; + + int length = text.length(); + int[] indexes = new int[length]; + for (int i = 0; i < length; i++) { + char ch = text.charAt(i); + indexes[i] = Arrays.binarySearch(characters, ch); + } + + return indexes; + } + + public float getTextWidth(String text) { + char[] characters = mCharacters; + float[] widths = mWidths; + float width = 0.0f; + + for (int i = 0, n = text.length(); i < n; i++) { + char ch = text.charAt(i); + int index = Arrays.binarySearch(characters, ch); + if (index >= 0) { + width += widths[index]; + } else { + width += mMaxWidth; + } + } + + return width; + } + + public float getTextWidth(int[] indexes) { + float[] widths = mWidths; + float width = 0.0f; + int length = indexes.length; + for (int i = 0; i < length; i++) { + int index = indexes[i]; + if (index >= 0) { + width += widths[index]; + } else { + width += mMaxWidth; + } + } + + return width; + } + + public float getMaxWidth() { + return mMaxWidth; + } + + public float getTextHeight() { + return mHeight; + } + + public void drawText(GLCanvas canvas, String text, int x, int y) { + char[] characters = mCharacters; + float[] widths = mWidths; + + for (int i = 0, n = text.length(); i < n; i++) { + char ch = text.charAt(i); + int index = Arrays.binarySearch(characters, ch); + if (index >= 0) { + drawSprite(canvas, index, x, y); + x += widths[index]; + } else { + x += mMaxWidth; + } + } + } + + public void drawText(GLCanvas canvas, int[] indexes, int x, int y) { + float[] widths = mWidths; + + for (int i = 0, n = indexes.length; i < n; i++) { + int index = indexes[i]; + if (index >= 0) { + drawSprite(canvas, index, x, y); + x += widths[index]; + } else { + x += mMaxWidth; + } + } + } + + /** + * Create a TextTexture to draw text + * + * @param typeface the typeface + * @param size text size + * @param characters all Characters + * @return the TextTexture + */ + public static TextTexture create(Typeface typeface, int size, int color, char[] characters) { + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setTextSize(size); + paint.setColor(color); + paint.setTypeface(typeface); + + Paint.FontMetricsInt fmi = paint.getFontMetricsInt(); + int fixed = fmi.bottom; + int height = fmi.bottom - fmi.top; + + int length = characters.length; + float[] widths = new float[length]; + paint.getTextWidths(characters, 0, length, widths); + + // Caculate bitmap size + float maxWidth = 0.0f; + for (float f : widths) { + maxWidth = Math.max(maxWidth, f); + } + int hCount = (int) Math.ceil(Math.sqrt(height / maxWidth * length)); + int vCount = (int) Math.ceil(Math.sqrt(maxWidth / height * length)); + if (hCount * (vCount - 1) > length) { + vCount--; + } + if ((hCount - 1) * vCount > length) { + hCount--; + } + + Bitmap bitmap = Bitmap.createBitmap((int) Math.ceil(hCount * maxWidth), + (int) Math.ceil(vCount * height), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + canvas.translate(0, height - fixed); + + // Draw + int[] rects = new int[length * 4]; + int x = 0; + int y = 0; + for (int i = 0; i < length; i++) { + int offset = i * 4; + rects[offset + 0] = x; + rects[offset + 1] = y; + rects[offset + 2] = (int) widths[i]; + rects[offset + 3] = height; + + canvas.drawText(characters, i, 1, x, y, paint); + + if (i % hCount == hCount - 1) { + // The end of row + x = 0; + y += height; + } else { + x += maxWidth; + } + } + + return new TextTexture(bitmap, length, rects, characters, widths, height, maxWidth); + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/Texture.java b/app/src/main/java/com/hippo/gl/glrenderer/Texture.java new file mode 100644 index 000000000..23a5ddf8f --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/Texture.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + + +import android.graphics.RectF; + +// Texture is a rectangular image which can be drawn on GLCanvas. +// The isOpaque() function gives a hint about whether the texture is opaque, +// so the drawing can be done faster. +// +// This is the current texture hierarchy: +// +// Texture +// -- ColorTexture +// -- FadeInTexture +// -- BasicTexture +// -- UploadedTexture +// -- BitmapTexture +// -- Tile +// -- ResourceTexture +// -- NinePatchTexture +// -- CanvasTexture +// -- StringTexture +// +public interface Texture { + int getWidth(); + int getHeight(); + void draw(GLCanvas canvas, int x, int y); + void draw(GLCanvas canvas, int x, int y, int w, int h); + void draw(GLCanvas canvas, RectF source, RectF target); + boolean isOpaque(); +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/TextureUploader.java b/app/src/main/java/com/hippo/gl/glrenderer/TextureUploader.java new file mode 100644 index 000000000..34371585d --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/TextureUploader.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import com.hippo.gl.view.GLRoot; + +import java.util.ArrayDeque; + +public class TextureUploader implements GLRoot.OnGLIdleListener { + private static final int INIT_CAPACITY = 64; + private static final int QUOTA_PER_FRAME = 1; + + private final ArrayDeque mFgTextures = + new ArrayDeque(INIT_CAPACITY); + private final ArrayDeque mBgTextures = + new ArrayDeque(INIT_CAPACITY); + private final GLRoot mGLRoot; + private volatile boolean mIsQueued = false; + + public TextureUploader(GLRoot root) { + mGLRoot = root; + } + + public synchronized void clear() { + while (!mFgTextures.isEmpty()) { + mFgTextures.pop().setIsUploading(false); + } + while (!mBgTextures.isEmpty()) { + mBgTextures.pop().setIsUploading(false); + } + } + + // caller should hold synchronized on "this" + private void queueSelfIfNeed() { + if (mIsQueued) return; + mIsQueued = true; + mGLRoot.addOnGLIdleListener(this); + } + + public synchronized void addBgTexture(UploadedTexture t) { + if (t.isContentValid()) return; + mBgTextures.addLast(t); + t.setIsUploading(true); + queueSelfIfNeed(); + } + + public synchronized void addFgTexture(UploadedTexture t) { + if (t.isContentValid()) return; + mFgTextures.addLast(t); + t.setIsUploading(true); + queueSelfIfNeed(); + } + + private int upload(GLCanvas canvas, ArrayDeque deque, + int uploadQuota, boolean isBackground) { + while (uploadQuota > 0) { + UploadedTexture t; + synchronized (this) { + if (deque.isEmpty()) break; + t = deque.removeFirst(); + t.setIsUploading(false); + if (t.isContentValid()) continue; + + // this has to be protected by the synchronized block + // to prevent the inner bitmap get recycled + t.updateContent(canvas); + } + + // It will took some more time for a texture to be drawn for + // the first time. + // Thus, when scrolling, if a new column appears on screen, + // it may cause a UI jank even these textures are uploaded. + if (isBackground) t.draw(canvas, 0, 0); + --uploadQuota; + } + return uploadQuota; + } + + @Override + public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { + int uploadQuota = QUOTA_PER_FRAME; + uploadQuota = upload(canvas, mFgTextures, uploadQuota, false); + if (uploadQuota < QUOTA_PER_FRAME) mGLRoot.requestRender(); + upload(canvas, mBgTextures, uploadQuota, true); + synchronized (this) { + mIsQueued = !mFgTextures.isEmpty() || !mBgTextures.isEmpty(); + return mIsQueued; + } + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/TiledTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/TiledTexture.java new file mode 100644 index 000000000..c926ea785 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/TiledTexture.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.os.SystemClock; + +import com.hippo.gl.view.GLRoot; + +import java.util.ArrayDeque; +import java.util.ArrayList; + +// This class is similar to BitmapTexture, except the bitmap is +// split into tiles. By doing so, we may increase the time required to +// upload the whole bitmap but we reduce the time of uploading each tile +// so it make the animation more smooth and prevents jank. +public class TiledTexture implements Texture { + private static final int CONTENT_SIZE = 254; + private static final int BORDER_SIZE = 1; + private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE; + private static final int INIT_CAPACITY = 8; + + // We are targeting at 60fps, so we have 16ms for each frame. + // In this 16ms, we use about 4~8 ms to upload tiles. + private static final long UPLOAD_TILE_LIMIT = 4; // ms + + private static Tile sFreeTileHead = null; + private static final Object sFreeTileLock = new Object(); + + private static Bitmap sUploadBitmap; + private static Canvas sCanvas; + private static Paint sBitmapPaint; + private static Paint sPaint; + + private int mUploadIndex = 0; + + private final Tile[] mTiles; // Can be modified in different threads. + // Should be protected by "synchronized." + private final int mWidth; + private final int mHeight; + private final RectF mSrcRect = new RectF(); + private final RectF mDestRect = new RectF(); + + public static class Uploader implements GLRoot.OnGLIdleListener { + private final ArrayDeque mTextures = + new ArrayDeque<>(INIT_CAPACITY); + + private final GLRoot mGlRoot; + private boolean mIsQueued = false; + + public Uploader(GLRoot glRoot) { + mGlRoot = glRoot; + } + + public synchronized void clear() { + mTextures.clear(); + } + + public synchronized void addTexture(TiledTexture t) { + if (t.isReady()) return; + mTextures.addLast(t); + + if (mIsQueued) return; + mIsQueued = true; + mGlRoot.addOnGLIdleListener(this); + } + + @Override + public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { + ArrayDeque deque = mTextures; + synchronized (this) { + long now = SystemClock.uptimeMillis(); + long dueTime = now + UPLOAD_TILE_LIMIT; + while (now < dueTime && !deque.isEmpty()) { + TiledTexture t = deque.peekFirst(); + if (t.uploadNextTile(canvas)) { + deque.removeFirst(); + mGlRoot.requestRender(); + } + now = SystemClock.uptimeMillis(); + } + mIsQueued = !mTextures.isEmpty(); + + // return true to keep this listener in the queue + return mIsQueued; + } + } + } + + private static class Tile extends UploadedTexture { + public int offsetX; + public int offsetY; + public Bitmap bitmap; + public Tile nextFreeTile; + public int contentWidth; + public int contentHeight; + + @Override + public void setSize(int width, int height) { + contentWidth = width; + contentHeight = height; + mWidth = width + 2 * BORDER_SIZE; + mHeight = height + 2 * BORDER_SIZE; + mTextureWidth = TILE_SIZE; + mTextureHeight = TILE_SIZE; + } + + @Override + protected Bitmap onGetBitmap() { + // make a local copy of the reference to the bitmap, + // since it might be null'd in a different thread. b/8694871 + Bitmap localBitmapRef = bitmap; + bitmap = null; + + if (localBitmapRef != null) { + int x = BORDER_SIZE - offsetX; + int y = BORDER_SIZE - offsetY; + int r = localBitmapRef.getWidth() + x; + int b = localBitmapRef.getHeight() + y; + sCanvas.drawBitmap(localBitmapRef, x, y, sBitmapPaint); + localBitmapRef = null; + + // draw borders if need + if (x > 0) sCanvas.drawLine(x - 1, 0, x - 1, TILE_SIZE, sPaint); + if (y > 0) sCanvas.drawLine(0, y - 1, TILE_SIZE, y - 1, sPaint); + if (r < CONTENT_SIZE) sCanvas.drawLine(r, 0, r, TILE_SIZE, sPaint); + if (b < CONTENT_SIZE) sCanvas.drawLine(0, b, TILE_SIZE, b, sPaint); + } + + return sUploadBitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + // do nothing + } + } + + private static void freeTile(Tile tile) { + tile.invalidateContent(); + tile.bitmap = null; + synchronized (sFreeTileLock) { + tile.nextFreeTile = sFreeTileHead; + sFreeTileHead = tile; + } + } + + private static Tile obtainTile() { + synchronized (sFreeTileLock) { + Tile result = sFreeTileHead; + if (result == null) return new Tile(); + sFreeTileHead = result.nextFreeTile; + result.nextFreeTile = null; + return result; + } + } + + private boolean uploadNextTile(GLCanvas canvas) { + if (mUploadIndex == mTiles.length) return true; + + synchronized (mTiles) { + Tile next = mTiles[mUploadIndex++]; + + // Make sure tile has not already been recycled by the time + // this is called (race condition in onGLIdle) + if (next.bitmap != null) { + boolean hasBeenLoad = next.isLoaded(); + next.updateContent(canvas); + + // It will take some time for a texture to be drawn for the first + // time. When scrolling, we need to draw several tiles on the screen + // at the same time. It may cause a UI jank even these textures has + // been uploaded. + if (!hasBeenLoad) next.draw(canvas, 0, 0); + } + } + return mUploadIndex == mTiles.length; + } + + public TiledTexture(Bitmap bitmap) { + mWidth = bitmap.getWidth(); + mHeight = bitmap.getHeight(); + ArrayList list = new ArrayList(); + + for (int x = 0, w = mWidth; x < w; x += CONTENT_SIZE) { + for (int y = 0, h = mHeight; y < h; y += CONTENT_SIZE) { + Tile tile = obtainTile(); + tile.offsetX = x; + tile.offsetY = y; + tile.bitmap = bitmap; + tile.setSize( + Math.min(CONTENT_SIZE, mWidth - x), + Math.min(CONTENT_SIZE, mHeight - y)); + list.add(tile); + } + } + mTiles = list.toArray(new Tile[list.size()]); + } + + public boolean isReady() { + return mUploadIndex == mTiles.length; + } + + // Can be called in UI thread. + public void recycle() { + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + freeTile(mTiles[i]); + } + } + } + + public static void freeResources() { + sUploadBitmap = null; + sCanvas = null; + sBitmapPaint = null; + sPaint = null; + } + + public static void prepareResources() { + sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Config.ARGB_8888); + sCanvas = new Canvas(sUploadBitmap); + sBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + sBitmapPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); + sPaint = new Paint(); + sPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); + sPaint.setColor(Color.TRANSPARENT); + } + + // We want to draw the "source" on the "target". + // This method is to find the "output" rectangle which is + // the corresponding area of the "src". + // (x,y) target + // (x0,y0) source +---------------+ + // +----------+ | | + // | src | | output | + // | +--+ | linear map | +----+ | + // | +--+ | ----------> | | | | + // | | by (scaleX, scaleY) | +----+ | + // +----------+ | | + // Texture +---------------+ + // Canvas + private static void mapRect(RectF output, + RectF src, float x0, float y0, float x, float y, float scaleX, + float scaleY) { + output.set(x + (src.left - x0) * scaleX, + y + (src.top - y0) * scaleY, + x + (src.right - x0) * scaleX, + y + (src.bottom - y0) * scaleY); + } + + // Draws a mixed color of this texture and a specified color onto the + // a rectangle. The used color is: from * (1 - ratio) + to * ratio. + public void drawMixed(GLCanvas canvas, int color, float ratio, + int x, int y, int width, int height) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float scaleX = (float) width / mWidth; + float scaleY = (float) height / mHeight; + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + mapRect(dest, src, 0, 0, x, y, scaleX, scaleY); + src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); + canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect); + } + } + } + + public void drawMixed(GLCanvas canvas, int color, float ratio, + RectF source, RectF target) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float x0 = source.left; + float y0 = source.top; + float x = target.left; + float y = target.top; + float scaleX = target.width() / source.width(); + float scaleY = target.height() / source.height(); + + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + if (!src.intersect(source)) continue; + mapRect(dest, src, x0, y0, x, y, scaleX, scaleY); + src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); + canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect); + } + } + } + + // Draws the texture on to the specified rectangle. + @Override + public void draw(GLCanvas canvas, int x, int y, int width, int height) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float scaleX = (float) width / mWidth; + float scaleY = (float) height / mHeight; + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + mapRect(dest, src, 0, 0, x, y, scaleX, scaleY); + src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); + canvas.drawTexture(t, mSrcRect, mDestRect); + } + } + } + + // Draws a sub region of this texture on to the specified rectangle. + public void draw(GLCanvas canvas, RectF source, RectF target) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float x0 = source.left; + float y0 = source.top; + float x = target.left; + float y = target.top; + float scaleX = target.width() / source.width(); + float scaleY = target.height() / source.height(); + + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + if (!src.intersect(source)) continue; + mapRect(dest, src, x0, y0, x, y, scaleX, scaleY); + src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); + canvas.drawTexture(t, src, dest); + } + } + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + draw(canvas, x, y, mWidth, mHeight); + } + + @Override + public boolean isOpaque() { + return false; + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/UploadedTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/UploadedTexture.java new file mode 100644 index 000000000..22e0ded0c --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/UploadedTexture.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.opengl.GLUtils; + +import junit.framework.Assert; + +import java.util.HashMap; + +import javax.microedition.khronos.opengles.GL11; + +// UploadedTextures use a Bitmap for the content of the texture. +// +// Subclasses should implement onGetBitmap() to provide the Bitmap and +// implement onFreeBitmap(mBitmap) which will be called when the Bitmap +// is not needed anymore. +// +// isContentValid() is meaningful only when the isLoaded() returns true. +// It means whether the content needs to be updated. +// +// The user of this class should call recycle() when the texture is not +// needed anymore. +// +// By default an UploadedTexture is opaque (so it can be drawn faster without +// blending). The user or subclass can override it using setOpaque(). +public abstract class UploadedTexture extends BasicTexture { + + // To prevent keeping allocation the borders, we store those used borders here. + // Since the length will be power of two, it won't use too much memory. + private static HashMap sBorderLines = + new HashMap<>(); + private static BorderKey sBorderKey = new BorderKey(); + + @SuppressWarnings("unused") + private static final String TAG = "Texture"; + private boolean mContentValid = true; + + // indicate this textures is being uploaded in background + private boolean mIsUploading = false; + private boolean mOpaque = true; + private boolean mThrottled = false; + private static int sUploadedCount; + private static final int UPLOAD_LIMIT = 100; + + protected Bitmap mBitmap; + private int mBorder; + + protected UploadedTexture() { + this(false); + } + + protected UploadedTexture(boolean hasBorder) { + super(null, 0, STATE_UNLOADED); + if (hasBorder) { + setBorder(true); + mBorder = 1; + } + } + + protected void setIsUploading(boolean uploading) { + mIsUploading = uploading; + } + + public boolean isUploading() { + return mIsUploading; + } + + private static class BorderKey implements Cloneable { + public boolean vertical; + public Config config; + public int length; + + @Override + public int hashCode() { + int x = config.hashCode() ^ length; + return vertical ? x : -x; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof BorderKey)) return false; + BorderKey o = (BorderKey) object; + return vertical == o.vertical + && config == o.config && length == o.length; + } + + @Override + public BorderKey clone() { + try { + return (BorderKey) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); + } + } + } + + protected void setThrottled(boolean throttled) { + mThrottled = throttled; + } + + private static Bitmap getBorderLine( + boolean vertical, Config config, int length) { + BorderKey key = sBorderKey; + key.vertical = vertical; + key.config = config; + key.length = length; + Bitmap bitmap = sBorderLines.get(key); + if (bitmap == null) { + bitmap = vertical + ? Bitmap.createBitmap(1, length, config) + : Bitmap.createBitmap(length, 1, config); + sBorderLines.put(key.clone(), bitmap); + } + return bitmap; + } + + private Bitmap getBitmap() { + if (mBitmap == null) { + mBitmap = onGetBitmap(); + if (mWidth == UNSPECIFIED) { + int w = mBitmap.getWidth() + mBorder * 2; + int h = mBitmap.getHeight() + mBorder * 2; + setSize(w, h); + } + } + return mBitmap; + } + + private void freeBitmap() { + Assert.assertTrue(mBitmap != null); + onFreeBitmap(mBitmap); + mBitmap = null; + } + + @Override + public int getWidth() { + if (mWidth == UNSPECIFIED) getBitmap(); + return mWidth; + } + + @Override + public int getHeight() { + if (mWidth == UNSPECIFIED) getBitmap(); + return mHeight; + } + + protected abstract Bitmap onGetBitmap(); + + protected abstract void onFreeBitmap(Bitmap bitmap); + + public void invalidateContent() { + if (mBitmap != null) freeBitmap(); + mContentValid = false; + mWidth = UNSPECIFIED; + mHeight = UNSPECIFIED; + } + + /** + * Whether the content on GPU is valid. + */ + public boolean isContentValid() { + return isLoaded() && mContentValid; + } + + /** + * Updates the content on GPU's memory. + * @param canvas + */ + public void updateContent(GLCanvas canvas) { + if (!isLoaded()) { + if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) { + return; + } + uploadToCanvas(canvas); + } else if (!mContentValid) { + Bitmap bitmap = getBitmap(); + int format = GLUtils.getInternalFormat(bitmap); + int type = GLUtils.getType(bitmap); + canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type); + freeBitmap(); + mContentValid = true; + } + } + + public static void resetUploadLimit() { + sUploadedCount = 0; + } + + public static boolean uploadLimitReached() { + return sUploadedCount > UPLOAD_LIMIT; + } + + private void uploadToCanvas(GLCanvas canvas) { + + Bitmap bitmap = getBitmap(); + if (bitmap != null) { + try { + int bWidth = bitmap.getWidth(); + int bHeight = bitmap.getHeight(); + int texWidth = getTextureWidth(); + int texHeight = getTextureHeight(); + + Assert.assertTrue(bWidth <= texWidth && bHeight <= texHeight); + + // Upload the bitmap to a new texture. + mId = canvas.getGLId().generateTexture(); + canvas.setTextureParameters(this); + + if (bWidth == texWidth && bHeight == texHeight) { + canvas.initializeTexture(this, bitmap); + } else { + int format = GLUtils.getInternalFormat(bitmap); + int type = GLUtils.getType(bitmap); + Config config = bitmap.getConfig(); + + canvas.initializeTextureSize(this, format, type); + canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type); + + if (mBorder > 0) { + // Left border + Bitmap line = getBorderLine(true, config, texHeight); + canvas.texSubImage2D(this, 0, 0, line, format, type); + + // Top border + line = getBorderLine(false, config, texWidth); + canvas.texSubImage2D(this, 0, 0, line, format, type); + } + + // Right border + if (mBorder + bWidth < texWidth) { + Bitmap line = getBorderLine(true, config, texHeight); + canvas.texSubImage2D(this, mBorder + bWidth, 0, line, format, type); + } + + // Bottom border + if (mBorder + bHeight < texHeight) { + Bitmap line = getBorderLine(false, config, texWidth); + canvas.texSubImage2D(this, 0, mBorder + bHeight, line, format, type); + } + } + } finally { + freeBitmap(); + } + // Update texture state. + setAssociatedCanvas(canvas); + mState = STATE_LOADED; + mContentValid = true; + } else { + mState = STATE_ERROR; + throw new RuntimeException("Texture load fail, no bitmap"); + } + } + + @Override + protected boolean onBind(GLCanvas canvas) { + updateContent(canvas); + return isContentValid(); + } + + @Override + protected int getTarget() { + return GL11.GL_TEXTURE_2D; + } + + public void setOpaque(boolean isOpaque) { + mOpaque = isOpaque; + } + + @Override + public boolean isOpaque() { + return mOpaque; + } + + @Override + public void recycle() { + super.recycle(); + if (mBitmap != null) freeBitmap(); + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/XmlResourceTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/XmlResourceTexture.java new file mode 100644 index 000000000..c38f9fbed --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/XmlResourceTexture.java @@ -0,0 +1,79 @@ +/* + * Copyright 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 + * 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. + */ + +package com.hippo.gl.glrenderer; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.support.annotation.DrawableRes; +import android.support.v4.content.ContextCompat; + +public class XmlResourceTexture extends UploadedTexture { + + private Context mContext; + private int mResId; + private int mWidth; + private int mHeight; + + public XmlResourceTexture(Context context, @DrawableRes int resId) { + mContext = context; + mResId = resId; + setOpaque(false); + } + + public void setBound(int width, int height) { + mWidth = width; + mHeight = height; + invalidateContent(); + } + + @Override + protected Bitmap onGetBitmap() { + Drawable drawable = ContextCompat.getDrawable(mContext, mResId); + + int width = mWidth; + int height = mHeight; + if (width <= 0) { + width = drawable.getIntrinsicWidth(); + } + if (height <= 0) { + height = drawable.getIntrinsicHeight(); + } + if (width <= 0) { + width = 1; + } + if (height <= 0) { + height = 1; + } + + drawable.setBounds(0, 0, width, height); + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.draw(canvas); + + return bitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + if (!inFinalizer()) { + bitmap.recycle(); + } + } +} diff --git a/app/src/main/java/com/hippo/gl/image/AnimatedImage.java b/app/src/main/java/com/hippo/gl/image/AnimatedImage.java new file mode 100644 index 000000000..f25d13812 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/image/AnimatedImage.java @@ -0,0 +1,122 @@ +/* + * 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 + * 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. + */ + +package com.hippo.gl.image; + +public class AnimatedImage extends Image { + + private boolean mDecoded; + private int mCurrentFrame; + + private boolean mDirty = false; + + protected AnimatedImage(long nativeImage, int format, int width, int height) { + super(nativeImage, format, width, height); + mCurrentFrame = 0; + } + + protected void checkDecoded() { + if (!mDecoded) { + throw new IllegalStateException("Call decode() first!"); + } + } + + public boolean isDecoded() { + checkRecycled(); + + return mDecoded; + } + + public boolean decode() { + checkRecycled(); + + if (mDecoded) { + return true; + } + if (mNativeImage != 0) { + boolean result = nativeDecode(mNativeImage, mFormat); + mDecoded = true; + return result; + } else { + return false; + } + } + + public boolean isAnimated() { + checkRecycled(); + checkDecoded(); + + return getFrameCount() != 1; + } + + public int getFrameCount() { + checkRecycled(); + checkDecoded(); + + return nativeGetFrameCount(mNativeImage, mFormat); + } + + public int getCurrentFrame() { + checkRecycled(); + checkDecoded(); + + return mCurrentFrame; + } + + public void setCurrentFrame(int frame) { + checkRecycled(); + checkDecoded(); + + int frameCount = getFrameCount(); + if (frame < 0 || frame >= frameCount) { + throw new IndexOutOfBoundsException("Invalid frame " + frame + ", frame count is " + frameCount); + } + mCurrentFrame = frame; + if (mNativeImage != 0) { + nativeSetCurrentFrame(mNativeImage, mFormat, frame); + mDirty = true; + } + } + + public void advance() { + setCurrentFrame((mCurrentFrame + 1) % getFrameCount()); + } + + public int getCurrentDelay() { + return getDelay(mCurrentFrame); + } + + public int getDelay(int frame) { + checkRecycled(); + checkDecoded(); + + int frameCount = getFrameCount(); + if (frame < 0 || frame >= frameCount) { + throw new IndexOutOfBoundsException("Invalid frame " + frame + ", frame count is " + frameCount); + } + return nativeGetDelay(mNativeImage, mFormat, frame); + } + + @Override + public void recycle() { + super.recycle(); + } + + private static native boolean nativeDecode(long nativeImage, int format); + private static native int nativeGetFrameCount(long nativeImage, int format); + private static native void nativeSetCurrentFrame(long nativeImage, int format, int frame); + private static native int nativeGetDelay(long nativeImage, int format, int frame); +} diff --git a/app/src/main/java/com/hippo/gl/image/Image.java b/app/src/main/java/com/hippo/gl/image/Image.java new file mode 100644 index 000000000..0aaa9e847 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/image/Image.java @@ -0,0 +1,87 @@ +/* + * 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 + * 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. + */ + +package com.hippo.gl.image; + +import android.graphics.Bitmap; + +import com.hippo.yorozuya.io.InputStreamPipe; + +public class Image { + + protected long mNativeImage; + + protected final int mFormat; + protected final int mWidth; + protected final int mHeight; + + protected Image(long nativeImage, int format, int width, int height) { + mNativeImage = nativeImage; + mFormat = format; + mWidth = width; + mHeight = height; + } + + protected void checkRecycled() { + if (mNativeImage == 0) { + throw new IllegalStateException("Image is recycled"); + } + } + + public int getFormat() { + return mFormat; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public boolean copyPixels(int srcX, int srcY, Bitmap bitmap, int dstX, int dstY, + int width, int height, int defaultColor) { + checkRecycled(); + + return nativeCopyPixels(mNativeImage, mFormat, srcX, srcY, bitmap, + dstX, dstY, width, height, defaultColor); + } + + public void recycle() { + if (mNativeImage != 0) { + nativeRecycle(mNativeImage, mFormat); + mNativeImage = 0; + } + } + + public boolean isRecycled() { + return mNativeImage == 0; + } + + public static Image decodeStreamPipe(InputStreamPipe isPipe) { + return nativeDecodeStreamPipe(isPipe); + } + + static { + System.loadLibrary("image"); + } + + private static native Image nativeDecodeStreamPipe(InputStreamPipe isPipe); + private static native boolean nativeCopyPixels(long nativeImage, int format, int srcX, int srcY, + Bitmap bitmap, int dstX, int dstY, int width, int height, int otherColor); + private static native void nativeRecycle(long nativeImage, int format); +} diff --git a/app/src/main/java/com/hippo/gl/util/ApiHelper.java b/app/src/main/java/com/hippo/gl/util/ApiHelper.java new file mode 100644 index 000000000..5cb3dd856 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/util/ApiHelper.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.util; + +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.hardware.Camera; +import android.os.Build; +import android.provider.MediaStore.MediaColumns; +import android.view.View; +import android.view.WindowManager; + +import java.lang.reflect.Field; + +public class ApiHelper { + public interface VERSION_CODES { + // These value are copied from Build.VERSION_CODES + int GINGERBREAD_MR1 = 10; + int HONEYCOMB = 11; + int HONEYCOMB_MR1 = 12; + int HONEYCOMB_MR2 = 13; + int ICE_CREAM_SANDWICH = 14; + int ICE_CREAM_SANDWICH_MR1 = 15; + int JELLY_BEAN = 16; + int JELLY_BEAN_MR1 = 17; + int JELLY_BEAN_MR2 = 18; + } + + public static final boolean AT_LEAST_16 = Build.VERSION.SDK_INT >= 16; + + public static final boolean USE_888_PIXEL_FORMAT = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean ENABLE_PHOTO_EDITOR = + Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; + + public static final boolean HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE = + hasField(View.class, "SYSTEM_UI_FLAG_LAYOUT_STABLE"); + + public static final boolean HAS_VIEW_SYSTEM_UI_FLAG_HIDE_NAVIGATION = + hasField(View.class, "SYSTEM_UI_FLAG_HIDE_NAVIGATION"); + + public static final boolean HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT = + hasField(MediaColumns.class, "WIDTH"); + + public static final boolean HAS_REUSING_BITMAP_IN_BITMAP_REGION_DECODER = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_REUSING_BITMAP_IN_BITMAP_FACTORY = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_SET_BEAM_PUSH_URIS = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_SET_DEFALT_BUFFER_SIZE = hasMethod( + "android.graphics.SurfaceTexture", "setDefaultBufferSize", + int.class, int.class); + + public static final boolean HAS_RELEASE_SURFACE_TEXTURE = hasMethod( + "android.graphics.SurfaceTexture", "release"); + + public static final boolean HAS_SURFACE_TEXTURE = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_MTP = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1; + + public static final boolean HAS_AUTO_FOCUS_MOVE_CALLBACK = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_REMOTE_VIEWS_SERVICE = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_INTENT_EXTRA_LOCAL_ONLY = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_SET_SYSTEM_UI_VISIBILITY = + hasMethod(View.class, "setSystemUiVisibility", int.class); + + public static final boolean HAS_FACE_DETECTION; + static { + boolean hasFaceDetection = false; + try { + Class listenerClass = Class.forName( + "android.hardware.Camera$FaceDetectionListener"); + hasFaceDetection = + hasMethod(Camera.class, "setFaceDetectionListener", listenerClass) && + hasMethod(Camera.class, "startFaceDetection") && + hasMethod(Camera.class, "stopFaceDetection") && + hasMethod(Camera.Parameters.class, "getMaxNumDetectedFaces"); + } catch (Throwable t) { + } + HAS_FACE_DETECTION = hasFaceDetection; + } + + public static final boolean HAS_GET_CAMERA_DISABLED = + hasMethod(DevicePolicyManager.class, "getCameraDisabled", ComponentName.class); + + public static final boolean HAS_MEDIA_ACTION_SOUND = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_TIME_LAPSE_RECORDING = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_ZOOM_WHEN_RECORDING = + Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; + + public static final boolean HAS_CAMERA_FOCUS_AREA = + Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; + + public static final boolean HAS_CAMERA_METERING_AREA = + Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; + + public static final boolean HAS_MOTION_EVENT_TRANSFORM = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_EFFECTS_RECORDING = false; + + // "Background" filter does not have "context" input port in jelly bean. + public static final boolean HAS_EFFECTS_RECORDING_CONTEXT_INPUT = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1; + + public static final boolean HAS_GET_SUPPORTED_VIDEO_SIZE = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_SET_ICON_ATTRIBUTE = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_MEDIA_PROVIDER_FILES_TABLE = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_SURFACE_TEXTURE_RECORDING = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_ACTION_BAR = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + // Ex: View.setTranslationX. + public static final boolean HAS_VIEW_TRANSFORM_PROPERTIES = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_CAMERA_HDR = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1; + + public static final boolean HAS_OPTIONS_IN_MUTABLE = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean CAN_START_PREVIEW_IN_JPEG_CALLBACK = + Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; + + public static final boolean HAS_VIEW_PROPERTY_ANIMATOR = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1; + + public static final boolean HAS_POST_ON_ANIMATION = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_ANNOUNCE_FOR_ACCESSIBILITY = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_OBJECT_ANIMATION = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_GLES20_REQUIRED = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_ROTATION_ANIMATION = + hasField(WindowManager.LayoutParams.class, "rotationAnimation"); + + public static final boolean HAS_ORIENTATION_LOCK = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2; + + public static final boolean HAS_CANCELLATION_SIGNAL = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_MEDIA_MUXER = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2; + + public static final boolean HAS_DISPLAY_LISTENER = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1; + + public static int getIntFieldIfExists(Class klass, String fieldName, + Class obj, int defaultVal) { + try { + Field f = klass.getDeclaredField(fieldName); + return f.getInt(obj); + } catch (Exception e) { + return defaultVal; + } + } + + private static boolean hasField(Class klass, String fieldName) { + try { + klass.getDeclaredField(fieldName); + return true; + } catch (NoSuchFieldException e) { + return false; + } + } + + private static boolean hasMethod(String className, String methodName, + Class... parameterTypes) { + try { + Class klass = Class.forName(className); + klass.getDeclaredMethod(methodName, parameterTypes); + return true; + } catch (Throwable th) { + return false; + } + } + + private static boolean hasMethod( + Class klass, String methodName, Class ... paramTypes) { + try { + klass.getDeclaredMethod(methodName, paramTypes); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } +} diff --git a/app/src/main/java/com/hippo/gl/util/GalleryUtils.java b/app/src/main/java/com/hippo/gl/util/GalleryUtils.java new file mode 100644 index 000000000..fb6c011e8 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/util/GalleryUtils.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.util; + +import android.graphics.Color; + +import com.hippo.yorozuya.AssertUtils; + +public class GalleryUtils { + + private static final String TAG = "GalleryUtils"; + + public static float[] intColorToFloatARGBArray(int from) { + return new float[] { + Color.alpha(from) / 255f, + Color.red(from) / 255f, + Color.green(from) / 255f, + Color.blue(from) / 255f + }; + } + + public static int floatARGBArrayTointColor(float[] from) { + return Color.argb((int) (from[0] * 255), (int) (from[1] * 255), (int) (from[2] * 255), (int) (from[3] * 255)); + } + + // Below are used the detect using database in the render thread. It only + // works most of the time, but that's ok because it's for debugging only. + + private static volatile Thread sCurrentThread; + + public static void setRenderThread() { + sCurrentThread = Thread.currentThread(); + } + + public static boolean isRenderThread() { + return sCurrentThread == Thread.currentThread(); + } + + public static void assertInRenderThread() { + AssertUtils.assertEquals("Should not do this in render thread", sCurrentThread, Thread.currentThread()); + } +} diff --git a/app/src/main/java/com/hippo/gl/util/MotionEventHelper.java b/app/src/main/java/com/hippo/gl/util/MotionEventHelper.java new file mode 100644 index 000000000..ac757db06 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/util/MotionEventHelper.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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. + */ +package com.hippo.gl.util; + +import android.annotation.TargetApi; +import android.graphics.Matrix; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; + +public final class MotionEventHelper { + private MotionEventHelper() {} + + public static MotionEvent transformEvent(MotionEvent e, Matrix m) { + // We try to use the new transform method if possible because it uses + // less memory. + if (ApiHelper.HAS_MOTION_EVENT_TRANSFORM) { + return transformEventNew(e, m); + } else { + return transformEventOld(e, m); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private static MotionEvent transformEventNew(MotionEvent e, Matrix m) { + MotionEvent newEvent = MotionEvent.obtain(e); + newEvent.transform(m); + return newEvent; + } + + // This is copied from Input.cpp in the android framework. + private static MotionEvent transformEventOld(MotionEvent e, Matrix m) { + long downTime = e.getDownTime(); + long eventTime = e.getEventTime(); + int action = e.getAction(); + int pointerCount = e.getPointerCount(); + int[] pointerIds = getPointerIds(e); + PointerCoords[] pointerCoords = getPointerCoords(e); + int metaState = e.getMetaState(); + float xPrecision = e.getXPrecision(); + float yPrecision = e.getYPrecision(); + int deviceId = e.getDeviceId(); + int edgeFlags = e.getEdgeFlags(); + int source = e.getSource(); + int flags = e.getFlags(); + + // Copy the x and y coordinates into an array, map them, and copy back. + float[] xy = new float[pointerCoords.length * 2]; + for (int i = 0; i < pointerCount;i++) { + xy[2 * i] = pointerCoords[i].x; + xy[2 * i + 1] = pointerCoords[i].y; + } + m.mapPoints(xy); + for (int i = 0; i < pointerCount;i++) { + pointerCoords[i].x = xy[2 * i]; + pointerCoords[i].y = xy[2 * i + 1]; + pointerCoords[i].orientation = transformAngle( + m, pointerCoords[i].orientation); + } + + MotionEvent n = MotionEvent.obtain(downTime, eventTime, action, + pointerCount, pointerIds, pointerCoords, metaState, xPrecision, + yPrecision, deviceId, edgeFlags, source, flags); + + return n; + } + + private static int[] getPointerIds(MotionEvent e) { + int n = e.getPointerCount(); + int[] r = new int[n]; + for (int i = 0; i < n; i++) { + r[i] = e.getPointerId(i); + } + return r; + } + + private static PointerCoords[] getPointerCoords(MotionEvent e) { + int n = e.getPointerCount(); + PointerCoords[] r = new PointerCoords[n]; + for (int i = 0; i < n; i++) { + r[i] = new PointerCoords(); + e.getPointerCoords(i, r[i]); + } + return r; + } + + private static float transformAngle(Matrix m, float angleRadians) { + // Construct and transform a vector oriented at the specified clockwise + // angle from vertical. Coordinate system: down is increasing Y, right is + // increasing X. + float[] v = new float[2]; + v[0] = (float) Math.sin(angleRadians); + v[1] = (float) -Math.cos(angleRadians); + m.mapVectors(v); + + // Derive the transformed vector's clockwise angle from vertical. + float result = (float) Math.atan2(v[0], -v[1]); + if (result < -Math.PI / 2) { + result += Math.PI; + } else if (result > Math.PI / 2) { + result -= Math.PI; + } + return result; + } +} diff --git a/app/src/main/java/com/hippo/gl/view/AnimationTime.java b/app/src/main/java/com/hippo/gl/view/AnimationTime.java new file mode 100644 index 000000000..5ea601ec9 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/view/AnimationTime.java @@ -0,0 +1,45 @@ + +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.view; + +import android.os.SystemClock; + +// +// The animation time should ideally be the vsync time the frame will be +// displayed, but that is an unknown time in the future. So we use the system +// time just after eglSwapBuffers (when GLSurfaceView.onDrawFrame is called) +// as a approximation. +// +public class AnimationTime { + private static volatile long sTime; + + // Sets current time as the animation time. + public static void update() { + sTime = SystemClock.uptimeMillis(); + } + + // Returns the animation time. + public static long get() { + return sTime; + } + + public static long startTime() { + sTime = SystemClock.uptimeMillis(); + return sTime; + } +} diff --git a/app/src/main/java/com/hippo/gl/view/GLRoot.java b/app/src/main/java/com/hippo/gl/view/GLRoot.java new file mode 100644 index 000000000..d4dc4077f --- /dev/null +++ b/app/src/main/java/com/hippo/gl/view/GLRoot.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.view; + +import android.content.Context; +import android.graphics.Matrix; + +import com.hippo.gl.anim.CanvasAnimation; +import com.hippo.gl.glrenderer.GLCanvas; + +public interface GLRoot { + + // Listener will be called when GL is idle AND before each frame. + // Mainly used for uploading textures. + interface OnGLIdleListener { + boolean onGLIdle(GLCanvas canvas, boolean renderRequested); + } + + void addOnGLIdleListener(OnGLIdleListener listener); + void registerLaunchedAnimation(CanvasAnimation animation); + void requestRenderForced(); + void requestRender(); + void requestLayoutContentPane(); + + void lockRenderThread(); + void unlockRenderThread(); + + void setContentPane(GLView content); + void setOrientationSource(OrientationSource source); + int getDisplayRotation(); + int getCompensation(); + Matrix getCompensationMatrix(); + void freeze(); + void unfreeze(); + void setLightsOutMode(boolean enabled); + + Context getContext(); + + int getWidth(); + int getHeight(); +} diff --git a/app/src/main/java/com/hippo/gl/view/GLRootView.java b/app/src/main/java/com/hippo/gl/view/GLRootView.java new file mode 100644 index 000000000..973d54577 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/view/GLRootView.java @@ -0,0 +1,807 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Matrix; +import android.opengl.GLSurfaceView; +import android.os.Build; +import android.os.Parcelable; +import android.os.Process; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.MotionEvent; +import android.view.SurfaceHolder; + +import com.hippo.gl.anim.CanvasAnimation; +import com.hippo.gl.glrenderer.BasicTexture; +import com.hippo.gl.glrenderer.GLCanvas; +import com.hippo.gl.glrenderer.GLES20Canvas; +import com.hippo.gl.glrenderer.UploadedTexture; +import com.hippo.gl.util.ApiHelper; +import com.hippo.gl.util.GalleryUtils; +import com.hippo.gl.util.MotionEventHelper; +import com.hippo.yorozuya.AssertUtils; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; + +// The root component of all GLViews. The rendering is done in GL +// thread while the event handling is done in the main thread. To synchronize +// the two threads, the entry points of this package need to synchronize on the +// GLRootView instance unless it can be proved that the rendering +// thread won't access the same thing as the method. The entry points include: +// (1) The public methods of HeadUpDisplay +// (2) The public methods of CameraHeadUpDisplay +// (3) The overridden methods in GLRootView. +public class GLRootView extends GLSurfaceView + implements GLSurfaceView.Renderer, GLRoot { + private static final String TAG = "GLRootView"; + + private static final boolean DEBUG_FPS = false; + private int mFrameCount = 0; + private long mFrameCountingStart = 0; + + private static final boolean DEBUG_INVALIDATE = false; + private int mInvalidateColor = 0; + + private static final boolean DEBUG_DRAWING_STAT = false; + + private static final int FLAG_INITIALIZED = 1; + private static final int FLAG_NEED_LAYOUT = 2; + + private GL11 mGL; + private GLCanvas mCanvas; + private GLView mContentView; + + private OrientationSource mOrientationSource; + // mCompensation is the difference between the UI orientation on GLCanvas + // and the framework orientation. See OrientationManager for details. + private int mCompensation; + // mCompensationMatrix maps the coordinates of touch events. It is kept sync + // with mCompensation. + private Matrix mCompensationMatrix = new Matrix(); + private int mDisplayRotation; + + private int mFlags = FLAG_NEED_LAYOUT; + private volatile boolean mRenderRequested = false; + + private final ArrayList mAnimations = + new ArrayList<>(); + + private final ArrayDeque mIdleListeners = + new ArrayDeque<>(); + + private final IdleRunner mIdleRunner = new IdleRunner(); + + private final ReentrantLock mRenderLock = new ReentrantLock(); + private final Condition mFreezeCondition = + mRenderLock.newCondition(); + private boolean mFreeze; + + private long mLastDrawFinishTime; + private boolean mInDownState = false; + //private boolean mFirstDraw = true; + + public GLRootView(Context context) { + this(context, null); + } + + public GLRootView(Context context, AttributeSet attrs) { + super(context, attrs); + mFlags |= FLAG_INITIALIZED; + setBackgroundDrawable(null); + setEGLContextClientVersion(2); + + // do not need depth buffer + setEGLConfigChooser(new ComponentSizeChooser(8, 8, 8, 8, 0, 0)); + setRenderer(this); + // setFormat is done by SurfaceView in SDK 2.3 and newer. + //if (ApiHelper.USE_888_PIXEL_FORMAT) { + // getHolder().setFormat(PixelFormat.RGB_888); + //} else { + // getHolder().setFormat(PixelFormat.RGB_565); + //} + + // Uncomment this to enable gl error check. + // setDebugFlags(DEBUG_CHECK_GL_ERROR); + } + + @Override + public void registerLaunchedAnimation(CanvasAnimation animation) { + // Register the newly launched animation so that we can set the start + // time more precisely. (Usually, it takes much longer for first + // rendering, so we set the animation start time as the time we + // complete rendering) + mAnimations.add(animation); + } + + @Override + public void addOnGLIdleListener(OnGLIdleListener listener) { + synchronized (mIdleListeners) { + mIdleListeners.addLast(listener); + if (mCanvas != null) { + // Wait for onSurfaceCreated + mIdleRunner.enable(); + } + } + } + + @Override + public void setContentPane(GLView content) { + if (mContentView == content) return; + if (mContentView != null) { + if (mInDownState) { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mContentView.dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + mInDownState = false; + } + mContentView.detachFromRoot(); + BasicTexture.yieldAllTextures(); + } + mContentView = content; + if (content != null) { + content.attachToRoot(this); + requestLayoutContentPane(); + } + } + + @Override + public void requestRenderForced() { + superRequestRender(); + } + + @Override + public void requestRender() { + if (DEBUG_INVALIDATE) { + StackTraceElement e = Thread.currentThread().getStackTrace()[4]; + String caller = e.getFileName() + ":" + e.getLineNumber() + " "; + Log.d(TAG, "invalidate: " + caller); + } + if (mRenderRequested) return; + mRenderRequested = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + postOnAnimation(mRequestRenderOnAnimationFrame); + } else { + super.requestRender(); + } + } + + private Runnable mRequestRenderOnAnimationFrame = new Runnable() { + @Override + public void run() { + superRequestRender(); + } + }; + + private void superRequestRender() { + super.requestRender(); + } + + @Override + public void requestLayoutContentPane() { + mRenderLock.lock(); + try { + if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return; + + // "View" system will invoke onLayout() for initialization(bug ?), we + // have to ignore it since the GLThread is not ready yet. + if ((mFlags & FLAG_INITIALIZED) == 0) return; + + mFlags |= FLAG_NEED_LAYOUT; + requestRender(); + } finally { + mRenderLock.unlock(); + } + } + + private void layoutContentPane() { + mFlags &= ~FLAG_NEED_LAYOUT; + + int w = getWidth(); + int h = getHeight(); + int displayRotation = 0; + int compensation = 0; + + // Get the new orientation values + if (mOrientationSource != null) { + displayRotation = mOrientationSource.getDisplayRotation(); + compensation = mOrientationSource.getCompensation(); + } else { + displayRotation = 0; + compensation = 0; + } + + if (mCompensation != compensation) { + mCompensation = compensation; + if (mCompensation % 180 != 0) { + mCompensationMatrix.setRotate(mCompensation); + // move center to origin before rotation + mCompensationMatrix.preTranslate(-w / 2, -h / 2); + // align with the new origin after rotation + mCompensationMatrix.postTranslate(h / 2, w / 2); + } else { + mCompensationMatrix.setRotate(mCompensation, w / 2, h / 2); + } + } + mDisplayRotation = displayRotation; + + // Do the actual layout. + if (mCompensation % 180 != 0) { + int tmp = w; + w = h; + h = tmp; + } + Log.i(TAG, "layout content pane " + w + "x" + h + " (compensation " + mCompensation + ")"); + if (mContentView != null && w != 0 && h != 0) { + mContentView.measure(GLView.MeasureSpec.makeMeasureSpec(w, GLView.MeasureSpec.EXACTLY), + GLView.MeasureSpec.makeMeasureSpec(h, GLView.MeasureSpec.EXACTLY)); + mContentView.layout(0, 0, w, h); + } + // Uncomment this to dump the view hierarchy. + //mContentView.dumpTree(""); + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (changed) requestLayoutContentPane(); + } + + /** + * Called when the context is created, possibly after automatic destruction. + */ + // This is a GLSurfaceView.Renderer callback + @Override + public void onSurfaceCreated(GL10 gl1, EGLConfig config) { + GL11 gl = (GL11) gl1; + if (mGL != null) { + // The GL Object has changed + Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl); + } + mRenderLock.lock(); + try { + mGL = gl; + mCanvas = new GLES20Canvas(); + BasicTexture.invalidateAllTextures(); + } finally { + mRenderLock.unlock(); + } + + if (DEBUG_FPS) { + setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + } else { + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + } + } + + /** + * Called when the OpenGL surface is recreated without destroying the + * context. + */ + // This is a GLSurfaceView.Renderer callback + @Override + public void onSurfaceChanged(GL10 gl1, int width, int height) { + Log.i(TAG, "onSurfaceChanged: " + width + "x" + height + ", gl10: " + gl1.toString()); + Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); + GalleryUtils.setRenderThread(); + GL11 gl = (GL11) gl1; + AssertUtils.assertTrue(mGL == gl); + + mCanvas.setSize(width, height); + } + + private void outputFps() { + long now = System.nanoTime(); + if (mFrameCountingStart == 0) { + mFrameCountingStart = now; + } else if ((now - mFrameCountingStart) > 1000000000) { + Log.d(TAG, "fps: " + (double) mFrameCount + * 1000000000 / (now - mFrameCountingStart)); + mFrameCountingStart = now; + mFrameCount = 0; + } + ++mFrameCount; + } + + @Override + public void onDrawFrame(GL10 gl) { + AnimationTime.update(); + + long t0 = System.nanoTime(); + + mRenderLock.lock(); + + while (mFreeze) { + mFreezeCondition.awaitUninterruptibly(); + } + + try { + onDrawFrameLocked(gl); + } finally { + mRenderLock.unlock(); + } + + // We put a black cover View in front of the SurfaceView and hide it + // after the first draw. This prevents the SurfaceView being transparent + // before the first draw. + //if (mFirstDraw) { + // mFirstDraw = false; + // post(new Runnable() { + // @Override + // public void run() { + // View root = getRootView(); + // View cover = root.findViewById(R.id.gl_root_cover); + // cover.setVisibility(GONE); + // } + // }); + //} + + long t = System.nanoTime(); + long duration = (t - t0) / 1000000; + + if (duration > 34) { + Log.v(TAG, "--- " + duration + " ---"); + } + } + + private void onDrawFrameLocked(GL10 gl) { + if (DEBUG_FPS) outputFps(); + // release the unbound textures and deleted buffers. + mCanvas.deleteRecycledResources(); + + // reset texture upload limit + UploadedTexture.resetUploadLimit(); + + mRenderRequested = false; + + if ((mOrientationSource != null + && mDisplayRotation != mOrientationSource.getDisplayRotation()) + || (mFlags & FLAG_NEED_LAYOUT) != 0) { + layoutContentPane(); + } + + mCanvas.save(GLCanvas.SAVE_FLAG_ALL); + rotateCanvas(-mCompensation); + if (mContentView != null) { + mContentView.render(mCanvas); + } else { + // Make sure we always draw something to prevent displaying garbage + mCanvas.clearBuffer(); + } + mCanvas.restore(); + + if (!mAnimations.isEmpty()) { + long now = AnimationTime.get(); + for (int i = 0, n = mAnimations.size(); i < n; i++) { + mAnimations.get(i).setStartTime(now); + } + mAnimations.clear(); + } + + if (UploadedTexture.uploadLimitReached()) { + requestRender(); + } + + synchronized (mIdleListeners) { + if (!mIdleListeners.isEmpty()) mIdleRunner.enable(); + } + + if (DEBUG_INVALIDATE) { + mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor); + mInvalidateColor = ~mInvalidateColor; + } + + if (DEBUG_DRAWING_STAT) { + mCanvas.dumpStatisticsAndClear(); + } + } + + private void rotateCanvas(int degrees) { + if (degrees == 0) return; + int w = getWidth(); + int h = getHeight(); + int cx = w / 2; + int cy = h / 2; + mCanvas.translate(cx, cy); + mCanvas.rotate(degrees, 0, 0, 1); + if (degrees % 180 != 0) { + mCanvas.translate(-cy, -cx); + } else { + mCanvas.translate(-cx, -cy); + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (!isEnabled()) return false; + + int action = event.getAction(); + if (action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_UP) { + mInDownState = false; + } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) { + return false; + } + + if (mCompensation != 0) { + event = MotionEventHelper.transformEvent(event, mCompensationMatrix); + } + + mRenderLock.lock(); + try { + // If this has been detached from root, we don't need to handle event + boolean handled = mContentView != null + && mContentView.dispatchTouchEvent(event); + if (action == MotionEvent.ACTION_DOWN && handled) { + mInDownState = true; + } + return handled; + } finally { + mRenderLock.unlock(); + } + } + + private class IdleRunner implements Runnable { + // true if the idle runner is in the queue + private boolean mActive = false; + + @Override + public void run() { + OnGLIdleListener listener; + synchronized (mIdleListeners) { + mActive = false; + if (mIdleListeners.isEmpty()) return; + listener = mIdleListeners.removeFirst(); + } + mRenderLock.lock(); + boolean keepInQueue; + try { + keepInQueue = listener.onGLIdle(mCanvas, mRenderRequested); + } finally { + mRenderLock.unlock(); + } + synchronized (mIdleListeners) { + if (keepInQueue) mIdleListeners.addLast(listener); + if (!mRenderRequested && !mIdleListeners.isEmpty()) enable(); + } + } + + public void enable() { + // Who gets the flag can add it to the queue + if (mActive) return; + mActive = true; + queueEvent(this); + } + } + + @Override + public void lockRenderThread() { + mRenderLock.lock(); + } + + @Override + public void unlockRenderThread() { + mRenderLock.unlock(); + } + + @Override + public void onPause() { + unfreeze(); + super.onPause(); + } + + @Override + public void setOrientationSource(OrientationSource source) { + mOrientationSource = source; + } + + @Override + public int getDisplayRotation() { + return mDisplayRotation; + } + + @Override + public int getCompensation() { + return mCompensation; + } + + @Override + public Matrix getCompensationMatrix() { + return mCompensationMatrix; + } + + @Override + public void freeze() { + mRenderLock.lock(); + mFreeze = true; + mRenderLock.unlock(); + } + + @Override + public void unfreeze() { + mRenderLock.lock(); + mFreeze = false; + mFreezeCondition.signalAll(); + mRenderLock.unlock(); + } + + @Override + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public void setLightsOutMode(boolean enabled) { + if (!ApiHelper.HAS_SET_SYSTEM_UI_VISIBILITY) return; + + int flags = 0; + if (enabled) { + flags = STATUS_BAR_HIDDEN; + if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) { + flags |= (SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + } + setSystemUiVisibility(flags); + } + + // We need to unfreeze in the following methods and in onPause(). + // These methods will wait on GLThread. If we have freezed the GLRootView, + // the GLThread will wait on main thread to call unfreeze and cause dead + // lock. + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + unfreeze(); + super.surfaceChanged(holder, format, w, h); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + unfreeze(); + super.surfaceCreated(holder); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + unfreeze(); + super.surfaceDestroyed(holder); + } + + @Override + protected void onDetachedFromWindow() { + unfreeze(); + super.onDetachedFromWindow(); + + // TODO Is it right ? + if (mContentView != null) { + mContentView.detachFromRoot(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void dispatchSaveInstanceState(@NonNull SparseArray container) { + super.dispatchSaveInstanceState(container); + if (mContentView != null) { + mContentView.saveHierarchyState(container); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void dispatchRestoreInstanceState(@NonNull SparseArray container) { + super.dispatchRestoreInstanceState(container); + if (mContentView != null) { + mContentView.restoreHierarchyState(container); + } + } + + @Override + protected void finalize() throws Throwable { + try { + unfreeze(); + } finally { + super.finalize(); + } + } + + /* + @SuppressWarnings("deprecation") + @Override + protected boolean fitSystemWindows(@NonNull Rect insets) { + int fitPaddingLeft = insets.left; + int fitPaddingTop = insets.top; + int fitPaddingRight = insets.right; + int fitPaddingBottom = insets.bottom; + + GLView contentView = mContentView; + if (contentView != null) { + for (int i = 0, n = contentView.getComponentCount(); i < n; i++) { + GLView component = contentView.getComponent(i); + if (component instanceof FitPaddingImpl) { + ((FitPaddingImpl) component).onFitPadding(fitPaddingLeft ,fitPaddingTop, + fitPaddingRight, fitPaddingBottom); + } + } + } + + insets.left = 0; + insets.top = 0; + insets.right = 0; + insets.bottom = 0; + + return super.fitSystemWindows(insets); + } + */ + + private static abstract class BaseConfigChooser implements EGLConfigChooser { + + public BaseConfigChooser(int[] configSpec) { + mConfigSpec = filterConfigSpec(configSpec); + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] num_config = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException( + "No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + EGLConfig config = chooseConfig(egl, display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + return config; + } + + abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs); + + protected int[] mConfigSpec; + + private int[] filterConfigSpec(int[] configSpec) { + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + int len = configSpec.length; + int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len-1); + newConfigSpec[len-1] = EGL10.EGL_RENDERABLE_TYPE; + newConfigSpec[len] = 4; // EGL14.EGL_OPENGL_ES2_BIT; /* EGL_OPENGL_ES2_BIT */ + newConfigSpec[len+1] = EGL10.EGL_NONE; + return newConfigSpec; + } + } + + /** + * No guarantee to choose a configuration with exactly the specified r,g,b,a,depth,stencil sizes. + * But we try our best. + */ + private static class ComponentSizeChooser extends BaseConfigChooser { + public ComponentSizeChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + super(new int[] { + EGL10.EGL_RED_SIZE, redSize, + EGL10.EGL_GREEN_SIZE, greenSize, + EGL10.EGL_BLUE_SIZE, blueSize, + EGL10.EGL_ALPHA_SIZE, alphaSize, + EGL10.EGL_DEPTH_SIZE, depthSize, + EGL10.EGL_STENCIL_SIZE, stencilSize, + EGL10.EGL_NONE}); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + // Use score to avoid "No config chosen" + int configIndex = 0; + int minScore = Integer.MAX_VALUE; + + for (int i = 0, n = configs.length; i < n; i++) { + EGLConfig config = configs[i]; + int d = findConfigAttrib(egl, display, config, + EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, + EGL10.EGL_STENCIL_SIZE, 0); + int r = findConfigAttrib(egl, display, config, + EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, + EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, + EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, + EGL10.EGL_ALPHA_SIZE, 0); + + int score = 0; + if (d < mDepthSize) { + score += 100; + } else if (d > mDepthSize) { + score += (d - mDepthSize); + } + if (s < mStencilSize) { + score += 100; + } else if (s > mStencilSize) { + score += (s - mStencilSize); + } + score += Math.abs(r - mRedSize); + score += Math.abs(g - mGreenSize); + score += Math.abs(b - mBlueSize); + score += Math.abs(a - mAlphaSize); + + if (score < minScore) { + minScore = score; + configIndex = i; + } + } + + return configs[configIndex]; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, + EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private int[] mValue; + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + } +} diff --git a/app/src/main/java/com/hippo/gl/view/GLView.java b/app/src/main/java/com/hippo/gl/view/GLView.java new file mode 100644 index 000000000..3e1e8cba9 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/view/GLView.java @@ -0,0 +1,1254 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.view; + +import android.graphics.Color; +import android.graphics.Rect; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.Log; +import android.util.SparseArray; +import android.view.MotionEvent; + +import com.hippo.gl.anim.CanvasAnimation; +import com.hippo.gl.glrenderer.GLCanvas; +import com.hippo.gl.glrenderer.GLPaint; +import com.hippo.yorozuya.AssertUtils; + +import java.util.ArrayList; + +// GLView is a UI component. It can render to a GLCanvas and accept touch +// events. A GLView may have zero or more child GLView and they form a tree +// structure. The rendering and event handling will pass through the tree +// structure. +// +// A GLView tree should be attached to a GLRoot before event dispatching and +// rendering happens. GLView asks GLRoot to re-render or re-layout the +// GLView hierarchy using requestRender() and requestLayoutContentPane(). +// +// The render() method is called in a separate thread. Before calling +// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the +// rendering thread running at the same time. If there are other entry points +// from main thread (like a Handler) in your GLView, you need to call +// lockRendering() if the rendering thread should not run at the same time. +// +public class GLView implements TouchOwner { + private static final String TAG = "GLView"; + + private static final boolean DEBUG_DRAW_BOUNDS = false; + + public static final int VISIBLE = 0; + public static final int INVISIBLE = 1; + + private static final int FLAG_INVISIBLE = 1; + private static final int FLAG_SET_MEASURED_SIZE = 2; + private static final int FLAG_LAYOUT_REQUESTED = 4; + + /** + * Used to mark a GlView that has no ID. + */ + public static final int NO_ID = -1; + + public interface OnClickListener { + boolean onClick(GLView v); + } + + public interface OnLongClickListener { + boolean onLongClick(GLView v); + } + + protected final static GLPaint mDrawBoundsPaint; + + static { + mDrawBoundsPaint = new GLPaint(); + mDrawBoundsPaint.setColor(Color.RED); + mDrawBoundsPaint.setLineWidth(2); + } + + /** + * Position in parent, just like left, top, right, bottom in {@link android.view.View} + */ + protected final Rect mBounds = new Rect(); + /** + * Position in root + */ + private final int[] mPosition = new int[2]; + protected final Rect mPaddings = new Rect(); + + private GLRoot mRoot; + protected GLView mParent; + private ArrayList mComponents; + private GLView mMotionTarget; + + private CanvasAnimation mAnimation; + + private int mViewFlags = 0; + + protected int mMeasuredWidth = 0; + protected int mMeasuredHeight = 0; + + private int mLastWidthSpec = -1; + private int mLastHeightSpec = -1; + + protected int mScrollY = 0; + protected int mScrollX = 0; + protected int mScrollHeight = 0; + protected int mScrollWidth = 0; + + /** + * The GlView's identifier. + * + * @see #setId(int) + * @see #getId() + */ + private int mID = NO_ID; + + /** + * The minimum height of the view. We'll try our best to have the height + * of this view to at least this amount. + */ + private int mMinHeight; + + /** + * The minimum width of the view. We'll try our best to have the width + * of this view to at least this amount. + */ + private int mMinWidth; + + private LayoutParams mLayoutParams; + + private int mBackgroundColor = Color.TRANSPARENT; + + private TouchHelper mTouchHelper; + + private float mHotspotX; + private float mHotspotY; + + private boolean mPressed; + + private OnClickListener mOnClickListener; + private OnLongClickListener mOnLongClickListener; + + // Sets the visibility of this GLView (either GLView.VISIBLE or + // GLView.INVISIBLE). + public void setVisibility(int visibility) { + if (visibility == getVisibility()) return; + if (visibility == VISIBLE) { + mViewFlags &= ~FLAG_INVISIBLE; + } else { + mViewFlags |= FLAG_INVISIBLE; + } + onVisibilityChanged(visibility); + invalidate(); + } + + // Returns GLView.VISIBLE or GLView.INVISIBLE + public int getVisibility() { + return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE; + } + + // This should only be called on the content pane (the topmost GLView). + public void attachToRoot(GLRoot root) { + AssertUtils.assertTrue(mParent == null && mRoot == null); + onAttachToRoot(root); + } + + // This should only be called on the content pane (the topmost GLView). + void detachFromRoot() { + AssertUtils.assertTrue(mParent == null && mRoot != null); + onDetachFromRoot(); + } + + // Returns the number of children of the GLView. + public int getComponentCount() { + return mComponents == null ? 0 : mComponents.size(); + } + + // Returns the children for the given index. + public GLView getComponent(int index) { + if (mComponents == null) { + throw new ArrayIndexOutOfBoundsException(index); + } + return mComponents.get(index); + } + + // Adds a child to this GLView. + public void addComponent(GLView component) { + addComponent(component, -1, null); + } + + public void addComponent(GLView component, int index) { + addComponent(component, index, null); + } + + public void addComponent(GLView component, LayoutParams params) { + addComponent(component, -1, params); + } + + public void addComponent(GLView component, int index, LayoutParams params) { + // Make component is not null + if (component == null) { + throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup"); + } + // Make sure the component doesn't have a parent currently. + if (component.mParent != null) { + throw new IllegalStateException( + "Component " + this + " being added, but it already has a parent"); + } + + // Ensure index is valid + if (index < 0) { + index = getComponentCount(); + } + + // Ensure params is valid + if (params == null) { + params = generateDefaultLayoutParams(); + } else if (!checkLayoutParams(params)) { + params = generateLayoutParams(params); + } + + // Build parent-child links + if (mComponents == null) { + mComponents = new ArrayList<>(); + } + + mComponents.add(index, component); + component.mParent = this; + component.setLayoutParams(params); + + // If this is added after we have a root, tell the component. + if (mRoot != null) { + component.onAttachToRoot(mRoot); + } + } + + // Removes a child from this GLView. + public boolean removeComponent(GLView component) { + if (mComponents == null) return false; + if (mComponents.remove(component)) { + removeOneComponent(component); + return true; + } + return false; + } + + public boolean removeComponentAt(int index) { + if (mComponents == null) { + return false; + } + if (index >= 0 && index < getComponentCount()) { + GLView component = mComponents.remove(index); + removeOneComponent(component); + return true; + } else { + return false; + } + } + + // Removes all children of this GLView. + public void removeAllComponents() { + for (int i = 0, n = mComponents.size(); i < n; ++i) { + removeOneComponent(mComponents.get(i)); + } + mComponents.clear(); + } + + private void removeOneComponent(GLView component) { + if (mMotionTarget == component) { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + component.onDetachFromRoot(); + component.mParent = null; + } + + /** + * Sets the identifier for this GlView. The identifier does not have to be + * unique in this GlView's hierarchy. The identifier should be a positive + * number. + * + * @param id a number used to identify the view + */ + public void setId(int id) { + mID = id; + } + + /** + * Returns this GlView's identifier. + * + * @return a positive integer used to identify the view or {@link #NO_ID} + * if the view has no ID + */ + public int getId() { + return mID; + } + + public Rect bounds() { + return mBounds; + } + + @Override + public void setHotspot(float x, float y) { + mHotspotX = x; + mHotspotY = y; + } + + public float getHotspotX() { + return mHotspotX; + } + + public float getHotspotY() { + return mHotspotY; + } + + @Override + public boolean isEnabled() { + return true; // TODO + } + + @Override + public boolean isPressed() { + return mPressed; + } + + @Override + public void setPressed(boolean pressed) { + mPressed = pressed; + } + + @Override + public boolean isClickable() { + return mOnClickListener != null; + } + + @Override + public boolean isLongClickable() { + return mOnLongClickListener != null; + } + + @Override + public boolean performClick() { + if (mOnClickListener != null) { + return mOnClickListener.onClick(this); + } else { + return false; + } + } + + @Override + public boolean performLongClick() { + if (mOnLongClickListener != null) { + return mOnLongClickListener.onLongClick(this); + } else { + return false; + } + } + + public void setOnClickListener(OnClickListener listener) { + mOnClickListener = listener; + } + + public void setOnLongClickListener(OnLongClickListener listener) { + mOnLongClickListener = listener; + } + + @Override + public int getWidth() { + return mBounds.right - mBounds.left; + } + + @Override + public int getHeight() { + return mBounds.bottom - mBounds.top; + } + + /** + * Offset this GLView's vertical location by the specified number of pixels. + * + * @param offset the number of pixels to offset the view by + */ + public void offsetTopAndBottom(int offset) { + if (offset != 0){ + mBounds.offset(0, offset); + invalidate(); + } + } + + /** + * Offset this GLView's horizontal location by the specified amount of pixels. + * + * @param offset the number of pixels to offset the view by + */ + public void offsetLeftAndRight(int offset) { + if (offset != 0){ + mBounds.offset(offset, 0); + invalidate(); + } + } + + public GLRoot getGLRoot() { + return mRoot; + } + + // Request re-rendering of the view hierarchy. + // This is used for animation or when the contents changed. + public void invalidate() { + GLRoot root = getGLRoot(); + if (root != null) root.requestRender(); + } + + // Request re-layout of the view hierarchy. + public void requestLayout() { + mViewFlags |= FLAG_LAYOUT_REQUESTED; + mLastWidthSpec = -1; + mLastHeightSpec = -1; + if (mParent != null) { + mParent.requestLayout(); + } else { + // Is this a content pane ? + GLRoot root = getGLRoot(); + if (root != null) root.requestLayoutContentPane(); + } + } + + protected void render(GLCanvas canvas) { + // render background color + if (Color.alpha(mBackgroundColor) != 0) { + canvas.fillRect(0, 0, getWidth(), getHeight(), mBackgroundColor); + } + + // render content + onRender(canvas); + + // render child + canvas.save(); + for (int i = 0, n = getComponentCount(); i < n; ++i) { + renderChild(canvas, getComponent(i)); + } + canvas.restore(); + + // render bounds + if (DEBUG_DRAW_BOUNDS) { + canvas.drawRect(0, 0, getWidth(), getHeight(), mDrawBoundsPaint); + } + } + + public void onRender(GLCanvas canvas) { + } + + public void setBackgroundColor(int color) { + mBackgroundColor = color; + } + + protected void renderChild(GLCanvas canvas, GLView component) { + if (component.getVisibility() != GLView.VISIBLE + && component.mAnimation == null) return; + + int xOffset = component.mBounds.left - mScrollX; + int yOffset = component.mBounds.top - mScrollY; + + canvas.translate(xOffset, yOffset); + + CanvasAnimation anim = component.mAnimation; + if (anim != null) { + canvas.save(anim.getCanvasSaveFlags()); + if (anim.calculate(AnimationTime.get())) { + invalidate(); + } else { + component.mAnimation = null; + } + anim.apply(canvas); + } + component.render(canvas); + if (anim != null) canvas.restore(); + canvas.translate(-xOffset, -yOffset); + } + + protected boolean onTouch(MotionEvent event) { + if (mTouchHelper == null) { + mTouchHelper = new TouchHelper(this); + } + return mTouchHelper.onTouch(event); + } + + protected boolean dispatchTouchEvent(MotionEvent event, + int x, int y, GLView component, boolean checkBounds) { + Rect rect = component.mBounds; + int left = rect.left; + int top = rect.top; + if (!checkBounds || rect.contains(x, y)) { + event.offsetLocation(-left, -top); + if (component.dispatchTouchEvent(event)) { + event.offsetLocation(left, top); + return true; + } + event.offsetLocation(left, top); + } + return false; + } + + protected boolean dispatchTouchEvent(MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getAction(); + if (mMotionTarget != null) { + if (action == MotionEvent.ACTION_DOWN) { + MotionEvent cancel = MotionEvent.obtain(event); + cancel.setAction(MotionEvent.ACTION_CANCEL); + dispatchTouchEvent(cancel, x, y, mMotionTarget, false); + mMotionTarget = null; + } else { + dispatchTouchEvent(event, x, y, mMotionTarget, false); + if (action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_UP) { + mMotionTarget = null; + } + return true; + } + } + if (action == MotionEvent.ACTION_DOWN) { + // in the reverse rendering order + for (int i = getComponentCount() - 1; i >= 0; --i) { + GLView component = getComponent(i); + if (component.getVisibility() != GLView.VISIBLE) continue; + if (dispatchTouchEvent(event, x, y, component, true)) { + mMotionTarget = component; + return true; + } + } + } + return onTouch(event); + } + + /** + * Called by {@link GLRootView#dispatchSaveInstanceState(SparseArray)} to + * store this GlView hierarchy's frozen state into the given container. + * + * @param container The SparseArray in which to save the view's state. + */ + public void saveHierarchyState(SparseArray container) { + dispatchSaveInstanceState(container); + } + + protected void dispatchSaveInstanceState(SparseArray container) { + if (mID != NO_ID) { + Parcelable state = onSaveInstanceState(); + if (state != null) { + container.put(mID, state); + } + } + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).dispatchSaveInstanceState(container); + } + } + + protected Parcelable onSaveInstanceState() { + return null; + } + + /** + * Called by {@link GLRootView#dispatchRestoreInstanceState(SparseArray)} to + * restore this view hierarchy's frozen state from the given container. + * + * @param container The SparseArray which holds previously frozen states. + */ + public void restoreHierarchyState(SparseArray container) { + dispatchRestoreInstanceState(container); + } + + protected void dispatchRestoreInstanceState(SparseArray container) { + if (mID != NO_ID) { + Parcelable state = container.get(mID); + if (state != null) { + onRestoreInstanceState(state); + } + } + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).dispatchRestoreInstanceState(container); + } + } + + protected void onRestoreInstanceState(Parcelable state) { + if (state != null) { + throw new IllegalArgumentException("Please override onRestoreInstanceState"); + } + } + + public void setPaddings(int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) { + mPaddings.set(paddingLeft, paddingTop, paddingRight, paddingBottom); + } + + public void setPaddingLeft(int paddingLeft) { + mPaddings.left = paddingLeft; + } + + public void setPaddingTop(int paddingTop) { + mPaddings.top = paddingTop; + } + + public void setPaddingRight(int paddingRight) { + mPaddings.right = paddingRight; + } + + public void setPaddingBottom(int paddingBottom) { + mPaddings.bottom = paddingBottom; + } + + public Rect getPaddings() { + return mPaddings; + } + + // There is a little different between GLView.layout() and View.layout(). + // onMeasure() is not called in GLView.layout(). + // For the content view of GLRootView, GLView.measure() is called before GLView.layout(). + // For component, GLView.measure() in called in parent's GLView.onMeasure(). + // So it is OK. + public void layout(int left, int top, int right, int bottom) { + boolean sizeChanged = setBounds(left, top, right, bottom); + final boolean forceLayout = (mViewFlags & FLAG_LAYOUT_REQUESTED) == FLAG_LAYOUT_REQUESTED; + if (sizeChanged || forceLayout) { + onLayout(sizeChanged, left, top, right, bottom); + } + mViewFlags &= ~FLAG_LAYOUT_REQUESTED; + } + + private boolean setBounds(int left, int top, int right, int bottom) { + boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left) + || (bottom - top) != (mBounds.bottom - mBounds.top); + mBounds.set(left, top, right, bottom); + + // Update position in root + int[] position = mPosition; + int oldX = position[0]; + int oldY = position[1]; + if (mParent == null) { + position[0] = 0; + position[1] = 0; + } else { + mParent.getPositionInRoot(position); + // Apply offset in parent + position[0] += left; + position[1] += top; + } + int x = position[0]; + int y = position[1]; + if (x != oldX || y != oldY) { + onPositionInRootChanged(x, y, oldX, oldY); + } + + return sizeChanged; + } + + protected void onPositionInRootChanged(int x, int y, int oldX, int oldY) { + } + + public void getPositionInRoot(int[] position) { + AssertUtils.assertEquals("position should be 2 length int array", position.length, 2); + position[0] = mPosition[0]; + position[1] = mPosition[1]; + } + + /** + * Get the rect of the view clipped by root rect + * @param rect the result + */ + public void getValidRect(Rect rect) { + GLRoot root = mRoot; + if (root == null) { + rect.setEmpty(); + return; + } + + int x = mPosition[0]; + int y = mPosition[1]; + rect.set(x, y, x + getWidth(), y + getHeight()); + if (!rect.intersect(0, 0, root.getWidth(), root.getHeight())) { + rect.setEmpty(); + return; + } + + rect.offset(-x, -y); + } + + public void measure(int widthSpec, int heightSpec) { + final boolean forceLayout = (mViewFlags & FLAG_LAYOUT_REQUESTED) == FLAG_LAYOUT_REQUESTED; + final boolean isExactly = MeasureSpec.getMode(widthSpec) == MeasureSpec.EXACTLY && + MeasureSpec.getMode(heightSpec) == MeasureSpec.EXACTLY; + final boolean matchingSize = isExactly && + getMeasuredWidth() == MeasureSpec.getSize(widthSpec) && + getMeasuredHeight() == MeasureSpec.getSize(heightSpec); + if (forceLayout || !matchingSize && + (widthSpec != mLastWidthSpec || + heightSpec != mLastHeightSpec)) { + mLastWidthSpec = widthSpec; + mLastHeightSpec = heightSpec; + + mViewFlags &= ~FLAG_SET_MEASURED_SIZE; + onMeasure(widthSpec, heightSpec); + if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) { + throw new IllegalStateException(getClass().getName() + + " should call setMeasuredSize() in onMeasure()"); + } + } + } + + protected void onMeasure(int widthSpec, int heightSpec) { + setMeasuredSize(getDefaultSize(getSuggestedMinimumWidth(), widthSpec), + getDefaultSize(getSuggestedMinimumHeight(), heightSpec)); + } + + protected int getSuggestedMinimumHeight() { + return getMinimumHeight() + mPaddings.top + mPaddings.bottom; + } + + protected int getSuggestedMinimumWidth() { + return getMinimumWidth() + mPaddings.left + mPaddings.right; + } + + /** + * Returns the minimum height of the view. + * + * @return the minimum height the view will try to be. + * + * @see #setMinimumHeight(int) + */ + public int getMinimumHeight() { + return mMinHeight; + } + + /** + * Sets the minimum height of the view. It is not guaranteed the view will + * be able to achieve this minimum height (for example, if its parent layout + * constrains it with less available height). + * + * @param minHeight The minimum height the view will try to be. + * + * @see #getMinimumHeight() + */ + public void setMinimumHeight(int minHeight) { + mMinHeight = minHeight; + requestLayout(); + } + + /** + * Returns the minimum width of the view. + * + * @return the minimum width the view will try to be. + * + * @see #setMinimumWidth(int) + */ + public int getMinimumWidth() { + return mMinWidth; + } + + /** + * Sets the minimum width of the view. It is not guaranteed the view will + * be able to achieve this minimum width (for example, if its parent layout + * constrains it with less available width). + * + * @param minWidth The minimum width the view will try to be. + * + * @see #getMinimumWidth() + */ + public void setMinimumWidth(int minWidth) { + mMinWidth = minWidth; + requestLayout(); + } + + protected void setMeasuredSize(int width, int height) { + mViewFlags |= FLAG_SET_MEASURED_SIZE; + mMeasuredWidth = width; + mMeasuredHeight = height; + } + + public int getMeasuredWidth() { + return mMeasuredWidth; + } + + public int getMeasuredHeight() { + return mMeasuredHeight; + } + + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + } + + /** + * Utility to return a default begin. Uses the supplied number as left or top. + * + * @param size the parent size + * @param specSize the child size + * @param paddingBeign the padding begin + * @param paddingFinish the padding finish + * @param position the {@link Gravity#POSITION_BEGIN} or {@link Gravity#POSITION_CENTER} or + * {@link Gravity#POSITION_FINISH} + * @return the begin size + */ + public static int getDefaultBegin(int size, int specSize, int paddingBeign, int paddingFinish, + @Gravity.PositionMode int position) { + if (position == Gravity.POSITION_FINISH) { + return size - paddingFinish - specSize; + } else if (position == Gravity.POSITION_CENTER) { + return ((size - paddingBeign - paddingFinish) / 2) - (specSize / 2) + paddingBeign; + } else { + return paddingBeign; + } + } + + /** + * Utility to return a default size. Uses the supplied size if the + * MeasureSpec imposed no constraints. Will get larger if allowed + * by the MeasureSpec. + * + * It works differently from {@link android.view.View#getDefaultSize(int, int)}. + * For {@link MeasureSpec#AT_MOST}, size is suggestion size + * if it is small then spec size or it is not zero, + * otherwise spec size. + * + * @param size Default size for this view + * @param measureSpec Constraints imposed by the parent + * @return The size this view should be. + */ + public static int getDefaultSize(int size, int measureSpec) { + int result = size; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + result = size; + break; + case MeasureSpec.EXACTLY: + result = specSize; + break; + case MeasureSpec.AT_MOST: + return size == 0 ? specSize : Math.min(size, specSize); + } + return result; + } + + /** + * Utility to return a max size. Uses the supplied size if the + * MeasureSpec imposed no constraints. Will get larger if allowed + * by the MeasureSpec. + * + * It works the same as {@link android.view.View#getDefaultSize(int, int)}. + * + * @param size Default size for this view + * @param measureSpec Constraints imposed by the parent + * @return The size this view should be. + */ + public static int getMaxSize(int size, int measureSpec) { + int result = size; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + result = size; + break; + case MeasureSpec.EXACTLY: + case MeasureSpec.AT_MOST: + result = specSize; + break; + } + return result; + } + + public static int getComponentSpec(int spec, int childSize) { + int specMode = MeasureSpec.getMode(spec); + int specSize = MeasureSpec.getSize(spec); + + int size = Math.max(0, specSize); + + int resultSize = 0; + int resultMode = 0; + + switch (specMode) { + // Parent has imposed an exact size on us + case MeasureSpec.EXACTLY: + if (childSize >= 0) { + resultSize = childSize; + resultMode = MeasureSpec.EXACTLY; + } else if (childSize == LayoutParams.MATCH_PARENT) { + // Child wants to be our size. So be it. + resultSize = size; + resultMode = MeasureSpec.EXACTLY; + } else if (childSize == LayoutParams.WRAP_CONTENT) { + // Child wants to determine its own size. It can't be + // bigger than us. + resultSize = size; + resultMode = MeasureSpec.AT_MOST; + } + break; + + // Parent has imposed a maximum size on us + case MeasureSpec.AT_MOST: + if (childSize >= 0) { + // Child wants a specific size... so be it + resultSize = childSize; + resultMode = MeasureSpec.EXACTLY; + } else if (childSize == LayoutParams.MATCH_PARENT) { + // Child wants to be our size, but our size is not fixed. + // Constrain child to not be bigger than us. + resultSize = size; + resultMode = MeasureSpec.AT_MOST; + } else if (childSize == LayoutParams.WRAP_CONTENT) { + // Child wants to determine its own size. It can't be + // bigger than us. + resultSize = size; + resultMode = MeasureSpec.AT_MOST; + } + break; + + // Parent asked to see how big we want to be + case MeasureSpec.UNSPECIFIED: + if (childSize >= 0) { + // Child wants a specific size... let him have it + resultSize = childSize; + resultMode = MeasureSpec.EXACTLY; + } else if (childSize == LayoutParams.MATCH_PARENT) { + // Child wants to be our size... find out how big it should + // be + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + } else if (childSize == LayoutParams.WRAP_CONTENT) { + // Child wants to determine its own size.... find out how + // big it should be + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + } + break; + } + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); + } + + public static void measureComponent(GLView component, int widthSpec, int heightSpec) { + final LayoutParams lp = component.getLayoutParams(); + final int componentWidthSpec = getComponentSpec(widthSpec, lp.width); + final int componentHeightSpec = getComponentSpec(heightSpec, lp.height); + component.measure(componentWidthSpec, componentHeightSpec); + } + + public static void measureAllComponents(GLView parent, int widthSpec, int heightSpec) { + for (int i = 0, n = parent.getComponentCount(); i < n; i++) { + GLView component = parent.getComponent(i); + measureComponent(component, widthSpec, heightSpec); + } + } + + public LayoutParams getLayoutParams() { + return mLayoutParams; + } + + public void setLayoutParams(LayoutParams params) { + if (params == null) { + if (mParent == null) { + throw new NullPointerException("Layout parameters cannot be null"); + } else { + params = mParent.generateDefaultLayoutParams(); + } + } + mLayoutParams = params; + requestLayout(); + } + + protected boolean checkLayoutParams(LayoutParams p) { + return p != null; + } + + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + + protected LayoutParams generateLayoutParams(LayoutParams p) { + return new LayoutParams(p); + } + + /** + * Gets the bounds of the given descendant that relative to this view. + */ + public boolean getBoundsOf(GLView descendant, Rect out) { + int xOffset = 0; + int yOffset = 0; + GLView view = descendant; + while (view != this) { + if (view == null) return false; + Rect bounds = view.mBounds; + xOffset += bounds.left; + yOffset += bounds.top; + view = view.mParent; + } + out.set(xOffset, yOffset, xOffset + descendant.getWidth(), + yOffset + descendant.getHeight()); + return true; + } + + protected void onVisibilityChanged(int visibility) { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + GLView child = getComponent(i); + if (child.getVisibility() == GLView.VISIBLE) { + child.onVisibilityChanged(visibility); + } + } + } + + public void onAttachToRoot(GLRoot root) { + mRoot = root; + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onAttachToRoot(root); + } + } + + public void onDetachFromRoot() { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onDetachFromRoot(); + } + mRoot = null; + } + + public void lockRendering() { + if (mRoot != null) { + mRoot.lockRenderThread(); + } + } + + public void unlockRendering() { + if (mRoot != null) { + mRoot.unlockRenderThread(); + } + } + + // This is for debugging only. + // Dump the view hierarchy into log. + void dumpTree(String prefix) { + Log.d(TAG, prefix + getClass().getSimpleName()); + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).dumpTree(prefix + "...."); + } + } + + /** + * A MeasureSpec encapsulates the layout requirements passed from parent to child. + * Each MeasureSpec represents a requirement for either the width or the height. + * A MeasureSpec is comprised of a size and a mode. There are three possible + * modes: + *
+ *
UNSPECIFIED
+ *
+ * The parent has not imposed any constraint on the child. It can be whatever size + * it wants. + *
+ * + *
EXACTLY
+ *
+ * The parent has determined an exact size for the child. The child is going to be + * given those bounds regardless of how big it wants to be. + *
+ * + *
AT_MOST
+ *
+ * The child can be as large as it wants up to the specified size. + *
+ *
+ * + * MeasureSpecs are implemented as ints to reduce object allocation. This class + * is provided to pack and unpack the <size, mode> tuple into the int. + */ + public static class MeasureSpec { + private static final int MODE_SHIFT = 30; + private static final int MODE_MASK = 0x3 << MODE_SHIFT; + + /** + * Measure specification mode: The parent has not imposed any constraint + * on the child. It can be whatever size it wants. + */ + public static final int UNSPECIFIED = 0 << MODE_SHIFT; + + /** + * Measure specification mode: The parent has determined an exact size + * for the child. The child is going to be given those bounds regardless + * of how big it wants to be. + */ + public static final int EXACTLY = 1 << MODE_SHIFT; + + /** + * Measure specification mode: The child can be as large as it wants up + * to the specified size. + */ + public static final int AT_MOST = 2 << MODE_SHIFT; + + /** + * The max size allowed + */ + public static final int MAX_SIZE = ~MODE_MASK; + + /** + * Creates a measure specification based on the supplied size and mode. + * + * The mode must always be one of the following: + *
    + *
  • {@link #UNSPECIFIED}
  • + *
  • {@link #EXACTLY}
  • + *
  • {@link #AT_MOST}
  • + *
+ * + *

Note: On API level 17 and lower, makeMeasureSpec's + * implementation was such that the order of arguments did not matter + * and overflow in either value could impact the resulting MeasureSpec. + * {@link android.widget.RelativeLayout} was affected by this bug. + * Apps targeting API levels greater than 17 will get the fixed, more strict + * behavior.

+ * + * @param size the size of the measure specification + * @param mode the mode of the measure specification + * @return the measure specification based on size and mode + */ + public static int makeMeasureSpec(int size, int mode) { + return (size & ~MODE_MASK) | (mode & MODE_MASK); + } + + /** + * Extracts the mode from the supplied measure specification. + * + * @param measureSpec the measure specification to extract the mode from + * @return {@link #UNSPECIFIED}, + * {@link #AT_MOST} or + * {@link #EXACTLY} + */ + public static int getMode(int measureSpec) { + return (measureSpec & MODE_MASK); + } + + /** + * Extracts the size from the supplied measure specification. + * + * @param measureSpec the measure specification to extract the size from + * @return the size in pixels defined in the supplied measure specification + */ + public static int getSize(int measureSpec) { + return (measureSpec & ~MODE_MASK); + } + + /** + * Returns a String representation of the specified measure + * specification. + * + * @param measureSpec the measure specification to convert to a String + * @return a String with the following format: "MeasureSpec: MODE SIZE" + */ + public static String toString(int measureSpec) { + int mode = getMode(measureSpec); + int size = getSize(measureSpec); + + StringBuilder sb = new StringBuilder("MeasureSpec: "); + + if (mode == UNSPECIFIED) + sb.append("UNSPECIFIED "); + else if (mode == EXACTLY) + sb.append("EXACTLY "); + else if (mode == AT_MOST) + sb.append("AT_MOST "); + else + sb.append(mode).append(" "); + + sb.append(size); + return sb.toString(); + } + } + + /** + * The LayoutParams look like {@link android.view.ViewGroup.LayoutParams}, + * and work like it. + */ + public static class LayoutParams { + + /** + * Special value for the height or width requested by a View. + * MATCH_PARENT means that the view wants to be as big as its parent, + * minus the parent's padding, if any. Introduced in API Level 8. + */ + public static final int MATCH_PARENT = -1; + + /** + * Special value for the height or width requested by a View. + * WRAP_CONTENT means that the view wants to be just large enough to fit + * its own internal content, taking its own padding into account. + */ + public static final int WRAP_CONTENT = -2; + + /** + * Information about how wide the view wants to be. Can be one of the + * constants {@link #MATCH_PARENT} or {@link #WRAP_CONTENT} or an exact size. + */ + public int width; + + /** + * Information about how tall the view wants to be. Can be one of the + * constants {@link #MATCH_PARENT} or {@link #WRAP_CONTENT} or an exact size. + */ + public int height; + + /** + * Creates a new set of layout parameters with the specified width + * and height. + * + * @param width the width, either {@link #WRAP_CONTENT}, + * {@link #MATCH_PARENT}, or a fixed size in pixels + * @param height the height, either {@link #WRAP_CONTENT}, + * {@link #MATCH_PARENT}, or a fixed size in pixels + */ + public LayoutParams(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * Copy constructor. Clones the width and height values of the source. + * + * @param source The layout params to copy from. + */ + public LayoutParams(LayoutParams source) { + this.width = source.width; + this.height = source.height; + } + } + + /** + * LayoutParams with gravity inside + */ + public static class GravityLayoutParams extends LayoutParams { + + public int gravity = Gravity.NO_GRAVITY; + + public GravityLayoutParams(GLView.LayoutParams source) { + super(source); + + if (source instanceof GravityLayoutParams) { + gravity = ((GravityLayoutParams) source).gravity; + } + } + + public GravityLayoutParams(int width, int height) { + super(width, height); + } + } +} diff --git a/app/src/main/java/com/hippo/gl/view/Gravity.java b/app/src/main/java/com/hippo/gl/view/Gravity.java new file mode 100644 index 000000000..3fd670338 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/view/Gravity.java @@ -0,0 +1,82 @@ +package com.hippo.gl.view; + +import android.support.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public class Gravity { + + @IntDef({POSITION_BEGIN, POSITION_CENTER, POSITION_FINISH}) + @Retention(RetentionPolicy.SOURCE) + public @interface PositionMode {} + + @IntDef({HORIZONTAL, VERTICAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface DirectionMode {} + + public static final int NO_GRAVITY = 0x0000; + + public static final int LEFT = 0x0001; + public static final int TOP = 0x0002; + public static final int RIGHT = 0x0004; + public static final int BOTTOM = 0x0008; + + public static final int CENTER_HORIZONTAL = LEFT | RIGHT; + public static final int CENTER_VERTICAL = TOP | BOTTOM; + public static final int CENTER = CENTER_VERTICAL|CENTER_HORIZONTAL; + + public static final int HORIZONTAL_MASK = CENTER_HORIZONTAL; + public static final int VERTICAL_MASK = CENTER_VERTICAL; + + public static final int POSITION_BEGIN = 0; + public static final int POSITION_CENTER = 1; + public static final int POSITION_FINISH = 2; + + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + public static boolean centerHorizontal(int gravity) { + return (gravity & HORIZONTAL_MASK) == CENTER_HORIZONTAL; + } + + public static boolean right(int gravity) { + return (gravity & HORIZONTAL_MASK) == RIGHT; + } + + public static boolean left(int gravity) { + return (gravity & HORIZONTAL_MASK) == LEFT; + } + + public static boolean centerVertical(int gravity) { + return (gravity & VERTICAL_MASK) == CENTER_VERTICAL; + } + + public static boolean top(int gravity) { + return (gravity & VERTICAL_MASK) == TOP; + } + + public static boolean bottom(int gravity) { + return (gravity & VERTICAL_MASK) == BOTTOM; + } + + public static @PositionMode int getPosition(int gravity, @DirectionMode int direction) { + if (direction == HORIZONTAL) { + if (right(gravity)) { + return POSITION_FINISH; + } else if (centerHorizontal(gravity)) { + return POSITION_CENTER; + } else { + return POSITION_BEGIN; + } + } else { + if (bottom(gravity)) { + return POSITION_FINISH; + } else if (centerVertical(gravity)) { + return POSITION_CENTER; + } else { + return POSITION_BEGIN; + } + } + } +} diff --git a/app/src/main/java/com/hippo/gl/view/OrientationSource.java b/app/src/main/java/com/hippo/gl/view/OrientationSource.java new file mode 100644 index 000000000..a8b6f8967 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/view/OrientationSource.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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. + */ + +package com.hippo.gl.view; + +public interface OrientationSource { + int getDisplayRotation(); + int getCompensation(); +} diff --git a/app/src/main/java/com/hippo/gl/view/TouchHelper.java b/app/src/main/java/com/hippo/gl/view/TouchHelper.java new file mode 100644 index 000000000..42e307e35 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/view/TouchHelper.java @@ -0,0 +1,242 @@ +/* + * Copyright 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 + * 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. + */ + +package com.hippo.gl.view; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.hippo.yorozuya.LayoutUtils; +import com.hippo.yorozuya.SimpleHandler; + +public class TouchHelper { + + private TouchOwner mOwner; + + private boolean mPrePressed = false; + private boolean mHasPerformedLongPress = false; + + private CheckForLongPress mPendingCheckForLongPress = null; + private CheckForTap mPendingCheckForTap = null; + private PerformClick mPerformClick = null; + private UnsetPressedState mUnsetPressedState = null; + + /** + * Cache the touch slop from the context that created the view. + */ + private static int sTouchSlop = 8; + + public static void initialize(Context context) { + sTouchSlop = LayoutUtils.dp2pix(context, 8); // 8dp + } + + public TouchHelper(@NonNull TouchOwner owner) { + mOwner = owner; + } + + public boolean onTouch(MotionEvent event) { + TouchOwner owner = mOwner; + final float x = event.getX(); + final float y = event.getY(); + + if (event.getAction() == MotionEvent.ACTION_DOWN || + event.getAction() == MotionEvent.ACTION_MOVE) + owner.setHotspot(x, y); + + if (!owner.isEnabled()) { + if (event.getAction() == MotionEvent.ACTION_UP && owner.isPressed()) { + owner.setPressed(false); + } + // A disabled view that is clickable still consumes the touch + // events, it just doesn't respond to them. + return (owner.isClickable() || owner.isLongClickable()); + } + + if (owner.isClickable() || owner.isLongClickable()) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + if (owner.isPressed() || mPrePressed) { + if (mPrePressed) { + // The button is being released before we actually + // showed it as pressed. Make it show the pressed + // state now (before scheduling the click) to ensure + // the user sees it. + setPressed(true, x, y); + } + + if (!mHasPerformedLongPress) { + // This is a tap, so remove the longpress check + removeLongPressCallback(); + + // Use a Runnable and post this rather than calling + // performClick directly. This lets other visual state + // of the view update before click actions start. + if (mPerformClick == null) { + mPerformClick = new PerformClick(); + } + if (!SimpleHandler.getInstance().post(mPerformClick)) { + owner.performClick(); + } + } + + if (mUnsetPressedState == null) { + mUnsetPressedState = new UnsetPressedState(); + } + + if (mPrePressed) { + SimpleHandler.getInstance().postDelayed(mUnsetPressedState, + ViewConfiguration.getPressedStateDuration()); + } else if (!SimpleHandler.getInstance().post(mUnsetPressedState)) { + // If the post failed, unpress right now + mUnsetPressedState.run(); + } + + removeTapCallback(); + } + break; + + case MotionEvent.ACTION_DOWN: + mHasPerformedLongPress = false; + + mPrePressed = true; + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); + } + mPendingCheckForTap.x = event.getX(); + mPendingCheckForTap.y = event.getY(); + SimpleHandler.getInstance().postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + break; + + case MotionEvent.ACTION_CANCEL: + owner.setPressed(false); + removeTapCallback(); + removeLongPressCallback(); + break; + + case MotionEvent.ACTION_MOVE: + owner.setHotspot(x, y); + + // Be lenient about moving outside of buttons + if (!pointInView(x, y, sTouchSlop)) { + // Outside button + removeTapCallback(); + if (owner.isPressed()) { + // Remove any future long press/tap checks + removeLongPressCallback(); + + owner.setPressed(false); + } + } + break; + } + + return true; + } + + return false; + } + + private void setPressed(boolean pressed, float x, float y){ + mOwner.setPressed(pressed); + if (pressed) { + mOwner.setHotspot(x, y); + } + } + + /** + * Utility method to determine whether the given point, in local coordinates, + * is inside the view, where the area of the view is expanded by the slop factor. + * This method is called while processing touch-move events to determine if the event + * is still within the view. + */ + public boolean pointInView(float localX, float localY, float slop) { + return localX >= -slop && localY >= -slop && localX < (mOwner.getWidth() + slop) && + localY < (mOwner.getHeight() + slop); + } + + /** + * Remove the longpress detection timer. + */ + private void removeLongPressCallback() { + if (mPendingCheckForLongPress != null) { + SimpleHandler.getInstance().removeCallbacks(mPendingCheckForLongPress); + } + } + + /** + * Remove the tap detection timer. + */ + private void removeTapCallback() { + if (mPendingCheckForTap != null) { + mPrePressed = false; + SimpleHandler.getInstance().removeCallbacks(mPendingCheckForTap); + } + } + + private void checkForLongClick(int delayOffset) { + if (mOwner.isLongClickable()) { + mHasPerformedLongPress = false; + + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(); + } + SimpleHandler.getInstance().postDelayed(mPendingCheckForLongPress, + ViewConfiguration.getLongPressTimeout() - delayOffset); + } + } + + private final class CheckForLongPress implements Runnable { + + @Override + public void run() { + if (mOwner.isPressed()) { + if (mOwner.performLongClick()) { + mHasPerformedLongPress = true; + } + } + } + } + + private final class CheckForTap implements Runnable { + public float x; + public float y; + + @Override + public void run() { + mPrePressed = false; + setPressed(true, x, y); + checkForLongClick(ViewConfiguration.getTapTimeout()); + } + } + + private final class PerformClick implements Runnable { + + @Override + public void run() { + mOwner.performClick(); + } + } + + private final class UnsetPressedState implements Runnable { + + @Override + public void run() { + mOwner.setPressed(false); + } + } +} diff --git a/app/src/main/java/com/hippo/gl/view/TouchOwner.java b/app/src/main/java/com/hippo/gl/view/TouchOwner.java new file mode 100644 index 000000000..338652a64 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/view/TouchOwner.java @@ -0,0 +1,40 @@ +/* + * Copyright 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 + * 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. + */ + +package com.hippo.gl.view; + +public interface TouchOwner { + + void setHotspot(float x, float y); + + boolean isEnabled(); + + boolean isPressed(); + + void setPressed(boolean pressed); + + boolean isClickable(); + + boolean isLongClickable(); + + boolean performClick(); + + boolean performLongClick(); + + int getWidth(); + + int getHeight(); +} diff --git a/app/src/main/java/com/hippo/gl/widget/EdgeEffect.java b/app/src/main/java/com/hippo/gl/widget/EdgeEffect.java new file mode 100644 index 000000000..f9559d616 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/widget/EdgeEffect.java @@ -0,0 +1,368 @@ +/* + * Copyright 2016 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 + * 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. + */ + +package com.hippo.gl.widget; + +import android.graphics.Rect; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.hippo.gl.glrenderer.GLCanvas; + +/** + * Material design edge effect + */ +public class EdgeEffect { + + // Time it will take the effect to fully recede in ms + private static final int RECEDE_TIME = 600; + + // Time it will take before a pulled glow begins receding in ms + private static final int PULL_TIME = 167; + + // Time it will take in ms for a pulled glow to decay to partial strength before release + private static final int PULL_DECAY_TIME = 2000; + + private static final float MAX_ALPHA = 0.5f; + + private static final float MAX_GLOW_SCALE = 2.f; + + private static final float PULL_GLOW_BEGIN = 0.f; + + // Minimum velocity that will be absorbed + private static final int MIN_VELOCITY = 100; + // Maximum velocity, clamps at this value + private static final int MAX_VELOCITY = 10000; + + private static final float EPSILON = 0.001f; + + private static final double ANGLE = Math.PI / 6; + private static final float SIN = (float) Math.sin(ANGLE); + private static final float COS = (float) Math.cos(ANGLE); + + private float mGlowAlpha; + private float mGlowScaleY; + + private float mGlowAlphaStart; + private float mGlowAlphaFinish; + private float mGlowScaleYStart; + private float mGlowScaleYFinish; + + private long mStartTime; + private float mDuration; + + private final Interpolator mInterpolator; + + private static final int STATE_IDLE = 0; + private static final int STATE_PULL = 1; + private static final int STATE_ABSORB = 2; + private static final int STATE_RECEDE = 3; + private static final int STATE_PULL_DECAY = 4; + + private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f; + + private static final int VELOCITY_GLOW_FACTOR = 6; + + private int mState = STATE_IDLE; + + private float mPullDistance; + + private final Rect mBounds = new Rect(); + private int mColor; + private float mRadius; + private float mBaseGlowScale; + private float mDisplacement = 0.5f; + private float mTargetDisplacement = 0.5f; + + /** + * Construct a new EdgeEffect with a theme appropriate for the provided context. + */ + public EdgeEffect(int color) { + mColor = color; + mInterpolator = new DecelerateInterpolator(); + } + + /** + * Set the size of this edge effect in pixels. + * + * @param width Effect width in pixels + * @param height Effect height in pixels + */ + public void setSize(int width, int height) { + final float r = width * 0.75f / SIN; + final float y = COS * r; + final float h = r - y; + final float or = height * 0.75f / SIN; + final float oy = COS * or; + final float oh = or - oy; + + mRadius = r; + mBaseGlowScale = h > 0 ? Math.min(oh / h, 1.f) : 1.f; + + mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h)); + } + + /** + * Reports if this EdgeEffect's animation is finished. If this method returns false + * after a call to {@link #draw(GLCanvas)} the host widget should schedule another + * drawing pass to continue the animation. + * + * @return true if animation is finished, false if drawing should continue on the next frame. + */ + public boolean isFinished() { + return mState == STATE_IDLE; + } + + /** + * Immediately finish the current animation. + * After this call {@link #isFinished()} will return true. + */ + public void finish() { + mState = STATE_IDLE; + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + *

Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement + * of the pull point is known.

+ * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + */ + public void onPull(float deltaDistance) { + onPull(deltaDistance, 0.5f); + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + * @param displacement The displacement from the starting side of the effect of the point + * initiating the pull. In the case of touch this is the finger position. + * Values may be from 0-1. + */ + public void onPull(float deltaDistance, float displacement) { + final long now = AnimationUtils.currentAnimationTimeMillis(); + mTargetDisplacement = displacement; + if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) { + return; + } + if (mState != STATE_PULL) { + mGlowScaleY = Math.max(PULL_GLOW_BEGIN, mGlowScaleY); + } + mState = STATE_PULL; + + mStartTime = now; + mDuration = PULL_TIME; + + mPullDistance += deltaDistance; + + final float absdd = Math.abs(deltaDistance); + mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, + mGlowAlpha + (absdd * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); + + if (mPullDistance == 0) { + mGlowScaleY = mGlowScaleYStart = 0; + } else { + final float scale = Math.max(0, 1 - 1 / + (float) Math.sqrt(Math.abs(mPullDistance) * mBounds.height()) - 0.3f) / 0.7f; + + mGlowScaleY = mGlowScaleYStart = scale; + } + + mGlowAlphaFinish = mGlowAlpha; + mGlowScaleYFinish = mGlowScaleY; + } + + /** + * Call when the object is released after being pulled. + * This will begin the "decay" phase of the effect. After calling this method + * the host view should {@link android.view.View#invalidate()} and thereby + * draw the results accordingly. + */ + public void onRelease() { + mPullDistance = 0; + + if (mState != STATE_PULL && mState != STATE_PULL_DECAY) { + return; + } + + mState = STATE_RECEDE; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = RECEDE_TIME; + } + + /** + * Call when the effect absorbs an impact at the given velocity. + * Used when a fling reaches the scroll boundary. + * + *

When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller}, + * the method getCurrVelocity will provide a reasonable approximation + * to use here.

+ * + * @param velocity Velocity at impact in pixels per second. + */ + public void onAbsorb(int velocity) { + mState = STATE_ABSORB; + velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY); + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = 0.15f + (velocity * 0.02f); + + // The glow depends more on the velocity, and therefore starts out + // nearly invisible. + mGlowAlphaStart = 0.3f; + mGlowScaleYStart = Math.max(mGlowScaleY, 0.f); + + + // Growth for the size of the glow should be quadratic to properly + // respond + // to a user's scrolling speed. The faster the scrolling speed, the more + // intense the effect should be for both the size and the saturation. + mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f) / 2, 1.f); + // Alpha should change for the glow as well as size. + mGlowAlphaFinish = Math.max( + mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA)); + mTargetDisplacement = 0.5f; + } + + /** + * Set the color of this edge effect in argb. + * + * @param color Color in argb + */ + public void setColor(int color) { + mColor = color; + } + + /** + * Return the color of this edge effect in argb. + * @return The color of this edge effect in argb + */ + public int getColor() { + return mColor; + } + + /** + * Draw into the provided canvas. Assumes that the canvas has been rotated + * accordingly and the size has been set. The effect will be drawn the full + * width of X=0 to X=width, beginning from Y=0 and extending to some factor < + * 1.f of height. + * + * @param canvas Canvas to draw into + * @return true if drawing should continue beyond this frame to continue the + * animation + */ + public boolean draw(GLCanvas canvas) { + update(); + + canvas.save(); + + final float centerX = mBounds.centerX(); + final float centerY = mBounds.height() - mRadius; + + canvas.translate(-centerX, 0f); + canvas.scale(1.f, Math.min(mGlowScaleY, 1.f) * mBaseGlowScale, 0); + canvas.translate(centerX, 0f); + + final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f; + float translateX = mBounds.width() * displacement / 2; + + canvas.translate(translateX, 0); + canvas.fillOval(centerX, centerY, mRadius, mRadius, mColor); + canvas.restore(); + + boolean oneLastFrame = false; + if (mState == STATE_RECEDE && mGlowScaleY == 0) { + mState = STATE_IDLE; + oneLastFrame = true; + } + + return mState != STATE_IDLE || oneLastFrame; + } + + /** + * Return the maximum height that the edge effect will be drawn at given the original + * {@link #setSize(int, int) input size}. + * @return The maximum height of the edge effect + */ + public int getMaxHeight() { + return (int) (mBounds.height() * MAX_GLOW_SCALE + 0.5f); + } + + private void update() { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final float t = Math.min((time - mStartTime) / mDuration, 1.f); + + final float interp = mInterpolator.getInterpolation(t); + + mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp; + mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp; + mDisplacement = (mDisplacement + mTargetDisplacement) / 2; + + if (t >= 1.f - EPSILON) { + switch (mState) { + case STATE_ABSORB: + mState = STATE_RECEDE; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = RECEDE_TIME; + + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After absorb, the glow should fade to nothing. + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL: + mState = STATE_PULL_DECAY; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = PULL_DECAY_TIME; + + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After pull, the glow should fade to nothing. + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL_DECAY: + mState = STATE_RECEDE; + break; + case STATE_RECEDE: + mState = STATE_IDLE; + break; + } + } + } +} diff --git a/app/src/main/java/com/hippo/gl/widget/EdgeView.java b/app/src/main/java/com/hippo/gl/widget/EdgeView.java new file mode 100644 index 000000000..b4f93c984 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/widget/EdgeView.java @@ -0,0 +1,165 @@ +/* + * Copyright 2016 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 + * 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. + */ + +package com.hippo.gl.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.opengl.Matrix; +import android.support.annotation.IntDef; + +import com.hippo.ehviewer.R; +import com.hippo.gl.glrenderer.GLCanvas; +import com.hippo.gl.view.GLView; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public final class EdgeView extends GLView { + + @IntDef({TOP, LEFT, BOTTOM, RIGHT}) + @Retention(RetentionPolicy.SOURCE) + private @interface Direction {} + + @SuppressWarnings("unused") + private static final String TAG = "EdgeView"; + + public static final int TOP = 0; + public static final int LEFT = 1; + public static final int BOTTOM = 2; + public static final int RIGHT = 3; + + // Each edge effect has a transform matrix, and each matrix has 16 elements. + // We put all the elements in one array. These constants specify the + // starting index of each matrix. + private static final int TOP_M = TOP * 16; + private static final int LEFT_M = LEFT * 16; + private static final int BOTTOM_M = BOTTOM * 16; + private static final int RIGHT_M = RIGHT * 16; + + private EdgeEffect[] mEffect = new EdgeEffect[4]; + private float[] mMatrix = new float[4 * 16]; + + public EdgeView(Context context) { + final TypedArray a = context.obtainStyledAttributes( + R.styleable.Theme); + final int themeColor = a.getColor( + R.styleable.Theme_colorPrimary, 0xff666666); + a.recycle(); + int color = (themeColor & 0xffffff) | 0x33000000; + for (int i = 0; i < 4; i++) { + mEffect[i] = new EdgeEffect(color); + } + } + + @Override + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + if (!changeSize) return; + + int w = right - left; + int h = bottom - top; + for (int i = 0; i < 4; i++) { + if ((i & 1) == 0) { // top or bottom + mEffect[i].setSize(w, h); + } else { // left or right + mEffect[i].setSize(h, w); + } + } + + // Set up transforms for the four edges. Without transforms an + // EdgeEffect draws the TOP edge from (0, 0) to (w, Y * h) where Y + // is some factor < 1. For other edges we need to move, rotate, and + // flip the effects into proper places. + Matrix.setIdentityM(mMatrix, TOP_M); + Matrix.setIdentityM(mMatrix, LEFT_M); + Matrix.setIdentityM(mMatrix, BOTTOM_M); + Matrix.setIdentityM(mMatrix, RIGHT_M); + + Matrix.rotateM(mMatrix, LEFT_M, 90, 0, 0, 1); + Matrix.scaleM(mMatrix, LEFT_M, 1, -1, 1); + + Matrix.translateM(mMatrix, BOTTOM_M, 0, h, 0); + Matrix.scaleM(mMatrix, BOTTOM_M, 1, -1, 1); + + Matrix.translateM(mMatrix, RIGHT_M, w, 0, 0); + Matrix.rotateM(mMatrix, RIGHT_M, 90, 0, 0, 1); + } + + @Override + public void onRender(GLCanvas canvas) { + super.render(canvas); + boolean more = false; + for (int i = 0; i < 4; i++) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.multiplyMatrix(mMatrix, i * 16); + more |= mEffect[i].draw(canvas); + canvas.restore(); + } + if (more) { + invalidate(); + } + } + + // Called when the content is pulled away from the edge. + // offset is in pixels. direction is one of {TOP, LEFT, BOTTOM, RIGHT}. + public void onPull(float offset, @Direction int direction) { + int fullLength = ((direction & 1) == 0) ? getWidth() : getHeight(); + mEffect[direction].onPull(offset / fullLength); + if (!mEffect[direction].isFinished()) { + invalidate(); + } + } + + public void onPull(float offset, float position, @Direction int direction) { + int fullLength = ((direction & 1) == 0) ? getHeight() : getWidth(); + int fullPosition = ((direction & 1) == 0) ? getWidth() : getHeight(); + mEffect[direction].onPull(offset / fullLength, position / fullPosition); + if (!mEffect[direction].isFinished()) { + invalidate(); + } + } + + // Call when the object is released after being pulled. + public void onRelease() { + boolean more = false; + for (int i = 0; i < 4; i++) { + mEffect[i].onRelease(); + more |= !mEffect[i].isFinished(); + } + if (more) { + invalidate(); + } + } + + public void onRelease(@Direction int direction) { + EdgeEffect edgeEffect = mEffect[direction]; + edgeEffect.onRelease(); + if (!edgeEffect.isFinished()) { + invalidate(); + } + } + + // Call when the effect absorbs an impact at the given velocity. + // Used when a fling reaches the scroll boundary. velocity is in pixels + // per second. direction is one of {TOP, LEFT, BOTTOM, RIGHT}. + public void onAbsorb(int velocity, int direction) { + mEffect[direction].onAbsorb(velocity); + if (!mEffect[direction].isFinished()) { + invalidate(); + } + } +} diff --git a/app/src/main/java/com/hippo/gl/widget/FrameLayout.java b/app/src/main/java/com/hippo/gl/widget/FrameLayout.java new file mode 100644 index 000000000..e5ddde695 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/widget/FrameLayout.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 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 + * 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. + */ + +package com.hippo.gl.widget; + +import com.hippo.gl.view.GLView; +import com.hippo.gl.view.Gravity; + +public class FrameLayout extends GLView { + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int maxWidth = 0; + int maxHeight = 0; + for (int i = 0, n = getComponentCount(); i < n; i++) { + GLView component = getComponent(i); + measureComponent(component, widthSpec, heightSpec); + maxWidth = Math.max(maxWidth, component.getMeasuredWidth()); + maxHeight = Math.max(maxHeight, component.getMeasuredHeight()); + } + + // Consider min + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + + // The final + maxWidth = getDefaultSize(maxWidth, widthSpec); + maxHeight = getDefaultSize(maxHeight, heightSpec); + + setMeasuredSize(maxWidth, maxHeight); + + if (MeasureSpec.getSize(widthSpec) != MeasureSpec.EXACTLY || + MeasureSpec.getSize(heightSpec) != MeasureSpec.EXACTLY) { + // Measure again + measureAllComponents(this, MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY)); + } + } + + @Override + protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) { + int width = getWidth(); + int height = getHeight(); + + for (int i = 0, n = getComponentCount(); i < n; i++) { + GLView component = getComponent(i); + int measureWidth = component.getMeasuredWidth(); + int measureHeight = component.getMeasuredHeight(); + int componentLeft; + int componentTop; + + GravityLayoutParams lp = (GravityLayoutParams) component.getLayoutParams(); + int gravity = lp.gravity; + if (Gravity.centerHorizontal(gravity)) { + componentLeft = (width / 2) - (measureWidth / 2); + } else if (Gravity.right(gravity)) { + componentLeft = width - measureWidth; + } else { + componentLeft = 0; + } + if (Gravity.centerVertical(gravity)) { + componentTop = (height / 2) - (measureHeight / 2); + } else if (Gravity.bottom(gravity)) { + componentTop = height - measureHeight; + } else { + componentTop = 0; + } + + component.layout(componentLeft, componentTop, + componentLeft + measureWidth, componentTop + measureHeight); + } + } + + @Override + protected boolean checkLayoutParams(GLView.LayoutParams p) { + return p instanceof GravityLayoutParams; + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new GravityLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + + @Override + protected LayoutParams generateLayoutParams(GLView.LayoutParams p) { + return p == null ? generateDefaultLayoutParams() : new GravityLayoutParams(p); + } +} diff --git a/app/src/main/java/com/hippo/gl/widget/GLImageView.java b/app/src/main/java/com/hippo/gl/widget/GLImageView.java new file mode 100644 index 000000000..5f64b6031 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/widget/GLImageView.java @@ -0,0 +1,114 @@ +/* + * Copyright 2016 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 + * 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. + */ + +package com.hippo.gl.widget; + +import android.graphics.Rect; +import android.graphics.RectF; +import android.support.annotation.IntDef; + +import com.hippo.gl.glrenderer.GLCanvas; +import com.hippo.gl.glrenderer.Texture; +import com.hippo.gl.view.GLView; +import com.hippo.yorozuya.MathUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public class GLImageView extends GLView { + + @IntDef({SCALE_ORIGIN, SCALE_FIT_WIDTH, SCALE_FIT_HEIGHT, SCALE_FIT}) + @Retention(RetentionPolicy.SOURCE) + public @interface Scale {} + + @IntDef({START_POSITION_TOP_LEFT, START_POSITION_TOP_RIGHT, START_POSITION_BOTTOM_LEFT, + START_POSITION_BOTTOM_RIGHT, START_POSITION_CENTER}) + @Retention(RetentionPolicy.SOURCE) + public @interface StartPosition {} + + public static final int SCALE_ORIGIN = 0; + public static final int SCALE_FIT_WIDTH = 1; + public static final int SCALE_FIT_HEIGHT = 2; + public static final int SCALE_FIT = 3; + + public static final int START_POSITION_TOP_LEFT = 0; + public static final int START_POSITION_TOP_RIGHT = 1; + public static final int START_POSITION_BOTTOM_LEFT = 2; + public static final int START_POSITION_BOTTOM_RIGHT = 3; + public static final int START_POSITION_CENTER = 4; + + private Texture mTexture; + + private RectF mDst = new RectF(); + private RectF mSrcActual = new RectF(); + private RectF mDstActual = new RectF(); + private Rect mValidRect = new Rect(); + + private boolean mPositionInRootDirty = true; + + private void applyPositionInRoot() { + int width = mTexture.getWidth(); + int height = mTexture.getHeight(); + RectF dst = mDst; + RectF dstActual = mDstActual; + RectF srcActual = mSrcActual; + + dstActual.set(dst); + getValidRect(mValidRect); + if (dstActual.intersect(mValidRect.left, mValidRect.top, mValidRect.right, mValidRect.bottom)) { + srcActual.left = MathUtils.lerp(0, width, + MathUtils.delerp(dst.left, dst.right, dstActual.left)); + srcActual.right = MathUtils.lerp(0, width, + MathUtils.delerp(dst.left, dst.right, dstActual.right)); + srcActual.top = MathUtils.lerp(0, height, + MathUtils.delerp(dst.top, dst.bottom, dstActual.top)); + srcActual.bottom = MathUtils.lerp(0, height, + MathUtils.delerp(dst.top, dst.bottom, dstActual.bottom)); + } else { + // Can't be seen, set src and dst empty + srcActual.setEmpty(); + dstActual.setEmpty(); + } + } + + @Override + protected void onPositionInRootChanged(int x, int y, int oldX, int oldY) { + mPositionInRootDirty = true; + } + + @Override + public void onRender(GLCanvas canvas) { + Texture texture = mTexture; + if (texture == null) { + return; + } + + + + + + + if (mPositionInRootDirty) { + mPositionInRootDirty = false; + applyPositionInRoot(); + } else { + mSrcActual.set(0, 0, texture.getWidth(), texture.getHeight()); + mDstActual.set(mDst); + } + + texture.draw(canvas, mSrcActual, mDstActual); + } +} diff --git a/app/src/main/java/com/hippo/gl/widget/ImageView.java b/app/src/main/java/com/hippo/gl/widget/ImageView.java new file mode 100644 index 000000000..a96b1e2a3 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/widget/ImageView.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016 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 + * 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. + */ + +package com.hippo.gl.widget; + +import android.graphics.Color; + +import com.hippo.gl.glrenderer.GLCanvas; +import com.hippo.gl.glrenderer.ImageTexture; +import com.hippo.gl.image.Image; +import com.hippo.gl.view.GLView; + +public class ImageView extends GLView implements ImageTexture.Callback { + + private ImageTexture mImageTexture; + + public ImageView(ImageTexture imageTexture) { + mImageTexture = imageTexture; + imageTexture.setCallback(this); + setBackgroundColor(Color.WHITE); + } + + @Override + public void onRender(GLCanvas canvas) { + mImageTexture.draw(canvas, 0, 0); + } + + @Override + public void invalidateImage(Image image) { + invalidate(); + } +} diff --git a/app/src/main/java/com/hippo/gl/widget/LinearLayout.java b/app/src/main/java/com/hippo/gl/widget/LinearLayout.java new file mode 100644 index 000000000..9201be4a6 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/widget/LinearLayout.java @@ -0,0 +1,227 @@ +/* + * Copyright 2016 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 + * 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. + */ + +package com.hippo.gl.widget; + +import android.graphics.Rect; +import android.support.annotation.IntDef; + +import com.hippo.gl.view.GLView; +import com.hippo.gl.view.Gravity; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +public class LinearLayout extends GLView { + + @IntDef({HORIZONTAL, VERTICAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface OrientationMode {} + + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + private int mInterval = 0; + private int mOrientation = VERTICAL; + + private List mTempList = new ArrayList<>(); + + public void setInterval(int interval) { + if (mInterval != interval) { + mInterval = interval; + requestLayout(); + } + } + + /** + * Should the layout be a column or a row. + * @param orientation Pass {@link #HORIZONTAL} or {@link #VERTICAL}. Default + * value is {@link #HORIZONTAL}. + */ + public void setOrientation(@OrientationMode int orientation) { + if (mOrientation != orientation) { + mOrientation = orientation; + requestLayout(); + } + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int layoutWidthSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthSpec) - mPaddings.left - mPaddings.right, + MeasureSpec.getMode(widthSpec)); + int layoutHeightSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightSpec) - mPaddings.top - mPaddings.bottom, + MeasureSpec.getMode(heightSpec)); + + if (mOrientation == HORIZONTAL && MeasureSpec.getMode(widthSpec) == MeasureSpec.EXACTLY) { + int width = MeasureSpec.getSize(layoutWidthSpec); + float sumWeight = 0.0f; + + for (int i = 0, n = getComponentCount(); i < n; i++) { + final GLView component = getComponent(i); + final LayoutParams lp = (LayoutParams) component.getLayoutParams(); + + if (lp.weight > 0.0f) { + sumWeight += lp.weight; + mTempList.add(component); + } else { + measureComponent(component, layoutWidthSpec, layoutHeightSpec); + width -= component.getMeasuredWidth(); + } + } + + for (GLView component : mTempList) { + final LayoutParams lp = (LayoutParams) component.getLayoutParams(); + final int componentWidthSpec = MeasureSpec.makeMeasureSpec( + (int) (width * lp.weight / sumWeight), MeasureSpec.EXACTLY); + final int componentHeightSpec = getComponentSpec(layoutHeightSpec, lp.height); + component.measure(componentWidthSpec, componentHeightSpec); + } + + mTempList.clear(); + } else if (mOrientation == VERTICAL && MeasureSpec.getMode(heightSpec) == MeasureSpec.EXACTLY) { + int height = MeasureSpec.getSize(layoutHeightSpec); + float sumWeight = 0.0f; + + for (int i = 0, n = getComponentCount(); i < n; i++) { + final GLView component = getComponent(i); + final LayoutParams lp = (LayoutParams) component.getLayoutParams(); + + if (lp.weight > 0.0f) { + sumWeight += lp.weight; + mTempList.add(component); + } else { + measureComponent(component, layoutWidthSpec, layoutHeightSpec); + height -= component.getMeasuredHeight(); + } + } + + for (GLView component : mTempList) { + final LayoutParams lp = (LayoutParams) component.getLayoutParams(); + final int componentWidthSpec = getComponentSpec(layoutWidthSpec, lp.width); + final int componentHeightSpec = MeasureSpec.makeMeasureSpec( + (int) (height * lp.weight / sumWeight), MeasureSpec.EXACTLY); + component.measure(componentWidthSpec, componentHeightSpec); + } + + mTempList.clear(); + } else { + measureAllComponents(this, layoutWidthSpec, layoutHeightSpec); + } + + int maxWidth = 0; + int maxHeight = 0; + for (int i = 0, n = getComponentCount(); i < n; i++) { + GLView component = getComponent(i); + + if (mOrientation == VERTICAL) { + maxWidth = Math.max(maxWidth, component.getMeasuredWidth()); + if (i != 0) { + maxHeight += mInterval; + } + maxHeight += component.getMeasuredHeight(); + } else if (mOrientation == HORIZONTAL) { + maxHeight = Math.max(maxHeight, component.getMeasuredHeight()); + if (i != 0) { + maxWidth += mInterval; + } + maxWidth += component.getMeasuredWidth(); + } + } + + Rect paddings = getPaddings(); + maxWidth = maxWidth + paddings.left + paddings.right; + maxHeight = maxHeight + paddings.top + paddings.bottom; + + setMeasuredSize(getDefaultSize(maxWidth, widthSpec), getDefaultSize(maxHeight, heightSpec)); + } + + @Override + protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) { + int width = getWidth(); + int height = getHeight(); + Rect paddings = getPaddings(); + int componentLeft; + int componentTop; + + if (mOrientation == VERTICAL) { + componentTop = paddings.top; + + for (int i = 0, n = getComponentCount(); i < n; i++) { + GLView component = getComponent(i); + int measureWidth = component.getMeasuredWidth(); + int measureHeight = component.getMeasuredHeight(); + + LayoutParams lp = (LayoutParams) component.getLayoutParams(); + componentLeft = getDefaultBegin(width, measureWidth, paddings.left, paddings.right, + Gravity.getPosition(lp.gravity, Gravity.HORIZONTAL)); + component.layout(componentLeft, componentTop, + componentLeft + measureWidth, componentTop + measureHeight); + + componentTop += measureHeight + mInterval; + } + } else if (mOrientation == HORIZONTAL) { + componentLeft = paddings.left; + + for (int i = 0, n = getComponentCount(); i < n; i++) { + GLView component = getComponent(i); + int measureWidth = component.getMeasuredWidth(); + int measureHeight = component.getMeasuredHeight(); + + LayoutParams lp = (LayoutParams) component.getLayoutParams(); + componentTop = getDefaultBegin(height, measureHeight, paddings.top, paddings.bottom, + Gravity.getPosition(lp.gravity, Gravity.VERTICAL)); + component.layout(componentLeft, componentTop, + componentLeft + measureWidth, componentTop + measureHeight); + + componentLeft += measureWidth + mInterval; + } + } + } + + @Override + protected boolean checkLayoutParams(GLView.LayoutParams p) { + return p instanceof LayoutParams; + } + + @Override + protected GLView.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + + @Override + protected GLView.LayoutParams generateLayoutParams(GLView.LayoutParams p) { + return p == null ? generateDefaultLayoutParams() : new LayoutParams(p); + } + + public static class LayoutParams extends GravityLayoutParams { + + public float weight = 0.0f; + + public LayoutParams(GLView.LayoutParams source) { + super(source); + + if (source instanceof LayoutParams) { + weight = ((LayoutParams) source).weight; + } + } + + public LayoutParams(int width, int height) { + super(width, height); + } + } +} diff --git a/app/src/main/java/com/hippo/gl/widget/ScaledImageView.java b/app/src/main/java/com/hippo/gl/widget/ScaledImageView.java new file mode 100644 index 000000000..78bb77115 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/widget/ScaledImageView.java @@ -0,0 +1,413 @@ +/* + * Copyright 2016 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 + * 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. + */ + +package com.hippo.gl.widget; + +import android.graphics.Rect; +import android.graphics.RectF; + +import com.hippo.ehviewer.ui.gl.GalleryView; +import com.hippo.gl.glrenderer.GLCanvas; +import com.hippo.gl.glrenderer.ImageTexture; +import com.hippo.gl.image.Image; +import com.hippo.gl.view.GLView; +import com.hippo.yorozuya.MathUtils; + +public class ScaledImageView extends GLView implements ImageTexture.Callback { + + // TODO adjust scale max and min accroding to image size and screen size + private static final float SCALE_MIN = 1 / 10.0f; + private static final float SCALE_MAX = 10.0f; + + public static final int ANIMATE_STATE_RUN = 0; + public static final int ANIMATE_STATE_STOP = 1; + + private int mAnimateState = ANIMATE_STATE_RUN; + + private ImageTexture mImageTexture; + + // Corresponding source is [0, 0, mTiledTexture.getWidth(), mTiledTexture.getHeight()] + private RectF mTarget = new RectF(); + private RectF mSeen = new RectF(); + private RectF mSourceAfterSeen = new RectF(); + private RectF mTargetAfterSeen = new RectF(); + private boolean mScaleOffsetDirty = true; + private boolean mSeenDirty = true; + + @GalleryView.Scale + private int mScaleMode; + @GalleryView.StartPosition + private int mStartPosition; + private float mLastScale; + private float mScale = 1f; + + public void setAnimateState(int animateState) { + + + + if (mAnimateState != animateState) { + mAnimateState = animateState; + + if (mImageTexture != null) { + mImageTexture.setPause(mAnimateState == ANIMATE_STATE_STOP); + } + } + } + + public void setImageTexture(ImageTexture imageTexture) { + ImageTexture oldImageTexture = mImageTexture; + if (oldImageTexture != null) { + oldImageTexture.setCallback(null); + } + + mImageTexture = imageTexture; + setScaleOffset(mScaleMode, mStartPosition, mLastScale); + + if (mImageTexture != null) { + mImageTexture.setCallback(this); + mImageTexture.setPause(mAnimateState == ANIMATE_STATE_STOP); + } + + invalidate(); + } + + public ImageTexture getImageTexture() { + return mImageTexture; + } + + @Override + protected int getSuggestedMinimumWidth() { + return Math.max(super.getSuggestedMinimumWidth(), + mImageTexture == null ? 0 : mImageTexture.getWidth()); + } + + @Override + protected int getSuggestedMinimumHeight() { + return Math.max(super.getSuggestedMinimumHeight(), + mImageTexture == null ? 0 : mImageTexture.getHeight()); + } + + /** + * If target is shorter then screen, make it in screen center. If target is + * longer then parent, make sure target fill parent over + */ + private void adjustPosition() { + RectF target = mTarget; + int screenWidth = getWidth(); + int screenHeight = getHeight(); + float targetWidth = target.width(); + float targetHeight = target.height(); + + if (targetWidth > screenWidth) { + float fixXOffset = target.left; + if (fixXOffset > 0) { + target.left -= fixXOffset; + target.right -= fixXOffset; + } else if ((fixXOffset = screenWidth - target.right) > 0) { + target.left += fixXOffset; + target.right += fixXOffset; + } + } else { + float left = (screenWidth - targetWidth) / 2; + target.offsetTo(left, target.top); + } + if (targetHeight > screenHeight) { + float fixYOffset = target.top; + if (fixYOffset > 0) { + target.top -= fixYOffset; + target.bottom -= fixYOffset; + } else if ((fixYOffset = screenHeight - target.bottom) > 0) { + target.top += fixYOffset; + target.bottom += fixYOffset; + } + } else { + float top = (screenHeight - targetHeight) / 2; + target.offsetTo(target.left, top); + } + } + + public void setScaleOffset(@GalleryView.Scale int scale, + @GalleryView.StartPosition int startPosition, float lastScale) { + mScaleMode = scale; + mStartPosition = startPosition; + mLastScale = lastScale; + + int screenWidth = getWidth(); + int screenHeight = getHeight(); + + if (mImageTexture == null || screenWidth == 0 || screenHeight == 0) { + mScaleOffsetDirty = true; + return; + } + + int imageWidth = mImageTexture.getWidth(); + int imageHeight = mImageTexture.getHeight(); + + // Set scale + float targetWidth; + float targetHeight; + switch (scale) { + case GalleryView.SCALE_ORIGIN: + mScale = 1.0f; + targetWidth = imageWidth; + targetHeight = imageHeight; + break; + case GalleryView.SCALE_FIT_WIDTH: + mScale = (float) screenWidth / imageWidth; + targetWidth = screenWidth; + targetHeight = imageHeight * mScale; + break; + case GalleryView.SCALE_FIT_HEIGHT: + mScale = (float) screenHeight / imageHeight; + targetWidth = imageWidth * mScale; + targetHeight = screenHeight; + break; + case GalleryView.SCALE_FIT: + float scaleX = (float) screenWidth / imageWidth; + float scaleY = (float) screenHeight / imageHeight; + if (scaleX < scaleY) { + mScale = scaleX; + targetWidth = screenWidth; + targetHeight = imageHeight * scaleX; + } else { + mScale = scaleY; + targetWidth = imageWidth * scaleY; + targetHeight = screenHeight; + break; + } + break; + case GalleryView.SCALE_FIXED: + default: + mScale = lastScale; + targetWidth = imageWidth * lastScale; + targetHeight = imageHeight * lastScale; + break; + } + + // adjust scale, not too big, not too small + if (mScale < SCALE_MIN) { + mScale = SCALE_MIN; + targetWidth = imageWidth * SCALE_MIN; + targetHeight = imageHeight * SCALE_MIN; + } else if (mScale > SCALE_MAX) { + mScale = SCALE_MAX; + targetWidth = imageWidth * SCALE_MAX; + targetHeight = imageHeight * SCALE_MAX; + } + + RectF target = mTarget; + // set start position + switch (startPosition) { + case GalleryView.START_POSITION_TOP_LEFT: + target.left = 0; + target.top = 0; + break; + case GalleryView.START_POSITION_TOP_RIGHT: + target.left = screenWidth - targetWidth; + target.top = 0; + break; + case GalleryView.START_POSITION_BOTTOM_LEFT: + target.left = 0; + target.top = screenHeight - targetHeight; + break; + case GalleryView.START_POSITION_BOTTOM_RIGHT: + target.left = screenWidth - targetWidth; + target.top = screenHeight - targetHeight; + break; + case GalleryView.START_POSITION_CENTER: + default: + target.left = (screenWidth - targetWidth) / 2; + target.top = (screenHeight - targetHeight) / 2; + break; + } + + target.right = target.left + targetWidth; + target.bottom = target.top + targetHeight; + + // adjust position + adjustPosition(); + + mScaleOffsetDirty = false; + mSeenDirty = true; + } + + public void scroll(int dx, int dy, int[] remain) { + if (mImageTexture == null) { + remain[0] = dx; + remain[1] = dy; + return; + } + + RectF target = mTarget; + int screenWidth = getWidth(); + int screenHeight = getHeight(); + float targetWidth = target.width(); + float targetHeight = target.height(); + + if (targetWidth > screenWidth) { + target.left -= dx; + target.right -= dx; + + float fixXOffset = target.left; + if (fixXOffset > 0) { + target.left -= fixXOffset; + target.right -= fixXOffset; + remain[0] = -(int) fixXOffset; + } else if ((fixXOffset = screenWidth - target.right) > 0) { + target.left += fixXOffset; + target.right += fixXOffset; + remain[0] = (int) fixXOffset; + } else { + remain[0] = 0; + } + } else { + remain[0] = dx; + } + if (targetHeight > screenHeight) { + target.top -= dy; + target.bottom -= dy; + + float fixYOffset = target.top; + if (fixYOffset > 0) { + target.top -= fixYOffset; + target.bottom -= fixYOffset; + remain[1] = -(int) fixYOffset; + } else if ((fixYOffset = screenHeight - target.bottom) > 0) { + target.top += fixYOffset; + target.bottom += fixYOffset; + remain[1] = (int) fixYOffset; + } else { + remain[1] = 0; + } + } else { + remain[1] = dy; + } + + if (dx != remain[0] || dy != remain[1]) { + mSeenDirty = true; + invalidate(); + } + } + + public void scale(float focusX, float focusY, float scale) { + if (mImageTexture == null) { + return; + } + + if ((mScale == SCALE_MAX && scale > 1f) || (mScale == SCALE_MIN && scale < 1f)) { + return; + } + + float newScale = mScale * scale; + newScale = MathUtils.clamp(newScale, SCALE_MIN, SCALE_MAX); + mScale = newScale; + RectF target = mTarget; + float left = (focusX - ((focusX - target.left) * scale)); + float top = (focusY - ((focusY - target.top) * scale)); + target.set(left, top, + (left + (mImageTexture.getWidth() * newScale)), + (top + (mImageTexture.getHeight() * newScale))); + + // adjust position + adjustPosition(); + + mSeenDirty = true; + invalidate(); + } + + public RectF getTargetRect() { + return mTarget; + } + + public float getScale() { + return mScale; + } + + public float getFitScale() { + int screenWidth = getWidth(); + int screenHeight = getHeight(); + + if (mImageTexture == null || screenWidth == 0 || screenHeight == 0) { + return 1f; + } + + int imageWidth = mImageTexture.getWidth(); + int imageHeight = mImageTexture.getHeight(); + + float scaleX = (float) screenWidth / imageWidth; + float scaleY = (float) screenHeight / imageHeight; + if (scaleX < scaleY) { + return scaleX; + } else { + return scaleY; + } + } + + public boolean isLoaded() { + return mImageTexture != null; + } + + public void setSeen(Rect seen) { + mSeen.set(seen); + mSeenDirty = true; + } + + public void applySeen() { + int width = mImageTexture.getWidth(); + int height = mImageTexture.getHeight(); + RectF target = mTarget; + RectF targetAfterSeen = mTargetAfterSeen; + RectF sourceAfterSeen = mSourceAfterSeen; + + targetAfterSeen.set(target); + if (targetAfterSeen.intersect(mSeen)) { + sourceAfterSeen.left = MathUtils.lerp(0, width, + MathUtils.delerp(target.left, target.right, targetAfterSeen.left)); + sourceAfterSeen.right = MathUtils.lerp(0, width, + MathUtils.delerp(target.left, target.right, targetAfterSeen.right)); + sourceAfterSeen.top = MathUtils.lerp(0, height, + MathUtils.delerp(target.top, target.bottom, targetAfterSeen.top)); + sourceAfterSeen.bottom = MathUtils.lerp(0, height, + MathUtils.delerp(target.top, target.bottom, targetAfterSeen.bottom)); + } else { + // Can't be seen + sourceAfterSeen.set(0f, 0f, 0f, 0f); + } + } + + @Override + public void onRender(GLCanvas canvas) { + if (mImageTexture != null) { + + if (mScaleOffsetDirty) { + setScaleOffset(mScaleMode, mStartPosition, mLastScale); + } + if (mSeenDirty) { + applySeen(); + mSeenDirty = false; + } + // Draw + if (!mSourceAfterSeen.isEmpty()) { + mImageTexture.draw(canvas, mSourceAfterSeen, mTargetAfterSeen); + } + } + } + + @Override + public void invalidateImage(Image image) { + invalidate(); + } +} diff --git a/app/src/main/java/com/hippo/gl/widget/TextView.java b/app/src/main/java/com/hippo/gl/widget/TextView.java new file mode 100644 index 000000000..e9535d2c1 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/widget/TextView.java @@ -0,0 +1,100 @@ +/* + * Copyright 2016 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 + * 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. + */ + +package com.hippo.gl.widget; + +import android.graphics.Rect; + +import com.hippo.gl.glrenderer.GLCanvas; +import com.hippo.gl.glrenderer.TextTexture; +import com.hippo.gl.view.GLView; +import com.hippo.gl.view.Gravity; +import com.hippo.yorozuya.ArrayUtils; + +public class TextView extends GLView { + + TextTexture mTextTexture; + + private String mText = ""; + private int[] mIndexes = ArrayUtils.EMPTY_INT_ARRAY; + + private int mGravity = Gravity.NO_GRAVITY; + + public TextView(TextTexture textTexture) { + mTextTexture = textTexture; + } + + public void setText(String text) { + if (text == null) { + text = ""; + } + if (!text.equals(mText)) { + int oldMinimumWidth = getSuggestedMinimumWidth(); + + mText = text; + mIndexes = mTextTexture.getTextIndexes(text); + + if (getSuggestedMinimumWidth() != oldMinimumWidth) { + requestLayout(); + } else { + invalidate(); + } + } + } + + public void setGravity(int gravity) { + if (mGravity != gravity) { + mGravity = gravity; + invalidate(); + } + } + + @Override + protected int getSuggestedMinimumWidth() { + if (mTextTexture == null) { + return super.getSuggestedMinimumWidth(); + } else { + return Math.max((int) mTextTexture.getTextWidth(mIndexes) + mPaddings.left + mPaddings.right, + super.getSuggestedMinimumWidth()); + } + } + + @Override + protected int getSuggestedMinimumHeight() { + if (mTextTexture == null) { + return super.getSuggestedMinimumHeight(); + } else { + return Math.max((int) mTextTexture.getTextHeight() + mPaddings.top + mPaddings.bottom, + super.getSuggestedMinimumHeight()); + } + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + setMeasuredSize(getDefaultSize(getSuggestedMinimumWidth(), widthSpec), + getDefaultSize(getSuggestedMinimumHeight(), heightSpec)); + } + + @Override + public void onRender(GLCanvas canvas) { + Rect paddings = getPaddings(); + int x = getDefaultBegin(getWidth(), (int) mTextTexture.getTextWidth(mIndexes), + paddings.left, paddings.right, Gravity.getPosition(mGravity, Gravity.HORIZONTAL)); + int y = getDefaultBegin(getHeight(), (int) mTextTexture.getTextHeight(), + paddings.top, paddings.bottom, Gravity.getPosition(mGravity, Gravity.VERTICAL)); + mTextTexture.drawText(canvas, mIndexes, x, y); + } +} diff --git a/app/src/main/java/com/hippo/gl/widget/XmlResourceView.java b/app/src/main/java/com/hippo/gl/widget/XmlResourceView.java new file mode 100644 index 000000000..bd3e1abf9 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/widget/XmlResourceView.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016 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 + * 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. + */ + +package com.hippo.gl.widget; + +import com.hippo.gl.glrenderer.GLCanvas; +import com.hippo.gl.glrenderer.XmlResourceTexture; +import com.hippo.gl.view.GLView; + +public class XmlResourceView extends GLView { + + private XmlResourceTexture mTexture; + + public void setTexture(XmlResourceTexture texture) { + mTexture = texture; + + if (texture != null) { + int width = getWidth(); + int height = getHeight(); + if (width > 0 && height > 0) { + texture.setBound(width, height); + invalidate(); + } + } + } + + @Override + protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) { + super.onLayout(changeSize, left, top, right, bottom); + + if (changeSize && mTexture != null) { + mTexture.setBound(right - left, bottom - top); + } + } + + @Override + public void onRender(GLCanvas canvas) { + super.onRender(canvas); + + if (mTexture != null) { + canvas.drawTexture(mTexture, 0, 0, getWidth(), getHeight()); + } + } +} diff --git a/app/src/main/res/layout/activity_gallery.xml b/app/src/main/res/layout/activity_gallery.xml new file mode 100644 index 000000000..9b10d9580 --- /dev/null +++ b/app/src/main/res/layout/activity_gallery.xml @@ -0,0 +1,22 @@ + + + + diff --git a/app/src/main/res/values-v19/themes.xml b/app/src/main/res/values-v19/themes.xml new file mode 100644 index 000000000..6d57f7a4a --- /dev/null +++ b/app/src/main/res/values-v19/themes.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4ceb168c1..db83468b5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -50,6 +50,8 @@ 登陆失败。这可能是网站更新导致的。你愿意跳过登陆么? 再按一次退出 点击重试 + 写!写!写! + 好痛 搜索 ExHentai @@ -73,7 +75,7 @@ 搜索低愿力标签 搜索差评标签 显示被删除的画廊 - 最小评分: + 最低评分: 2 星 @@ -140,11 +142,11 @@ 大小 压缩情况 收藏次数 - 是否被收藏 + 收藏 评价次数 评分 种子个数 - 种子页面链接 + 种子 画廊评论 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 65870e45e..0b14e5250 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -50,6 +50,8 @@ 登陸失敗。這可能是網站更新導致的。你願意跳過登陸麼? 再按一次退出 點選重試 + 寫!寫!寫! + 好痛 搜尋 ExHentai @@ -73,7 +75,7 @@ 搜尋低願力標籤 搜尋差評標籤 顯示被刪除的畫廊 - 最小評分: + 最低評分: 2 星 @@ -140,11 +142,11 @@ 大小 壓縮情況 收藏次數 - 是否被收藏 + 收藏 評價次數 評分 種子個數 - 種子頁面連結 + 種子 畫廊評論 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 46698b2dd..1a6c252c9 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -97,4 +97,7 @@ #38000000 + + #ff212121 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b68b0ea30..5ecdfed85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -89,6 +89,8 @@ Sign in failed. But perhaps it is a mistake due to website update. Do you want to skip signing in? Press twice to exit Click to retry + Write! Write! Write! + It hurts Search ExHentai diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index a1cd66fee..c4c31c12c 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -17,8 +17,10 @@ + + + + + + + + + + + + + + + + + + + +