From 01daf4a63eec9ce1c25c5f97bc51c88361f69c63 Mon Sep 17 00:00:00 2001 From: tarsin Date: Mon, 30 May 2022 09:20:02 +0800 Subject: [PATCH] Inline hippo's glcomponents * Replace android support with androidx as well --- app/build.gradle | 7 - .../com/hippo/glgallery/DownUpDetector.java | 67 + .../main/java/com/hippo/glgallery/Fling.java | 138 ++ .../com/hippo/glgallery/GalleryPageView.java | 197 +++ .../com/hippo/glgallery/GalleryProvider.java | 272 ++++ .../java/com/hippo/glgallery/GalleryView.java | 1238 +++++++++++++++ .../hippo/glgallery/GestureRecognizer.java | 175 +++ .../java/com/hippo/glgallery/ImageView.java | 604 ++++++++ .../hippo/glgallery/PagerLayoutManager.java | 1093 +++++++++++++ .../hippo/glgallery/ScrollLayoutManager.java | 1241 +++++++++++++++ .../com/hippo/glgallery/SimpleAdapter.java | 163 ++ .../com/hippo/glview/anim/AlphaAnimation.java | 48 + .../java/com/hippo/glview/anim/Animation.java | 167 ++ .../hippo/glview/anim/CanvasAnimation.java | 25 + .../com/hippo/glview/anim/FloatAnimation.java | 40 + .../hippo/glview/annotation/RenderThread.java | 27 + .../hippo/glview/glrenderer/BasicTexture.java | 221 +++ .../glview/glrenderer/BitmapTexture.java | 54 + .../glview/glrenderer/CanvasTexture.java | 51 + .../hippo/glview/glrenderer/ColorTexture.java | 74 + .../hippo/glview/glrenderer/ExtTexture.java | 60 + .../glview/glrenderer/FadeInTexture.java | 52 + .../glview/glrenderer/FadeOutTexture.java | 52 + .../hippo/glview/glrenderer/FadeTexture.java | 79 + .../com/hippo/glview/glrenderer/GLCanvas.java | 223 +++ .../hippo/glview/glrenderer/GLES11Canvas.java | 1080 +++++++++++++ .../hippo/glview/glrenderer/GLES11IdImpl.java | 67 + .../hippo/glview/glrenderer/GLES20Canvas.java | 1039 +++++++++++++ .../hippo/glview/glrenderer/GLES20IdImpl.java | 42 + .../com/hippo/glview/glrenderer/GLId.java | 33 + .../com/hippo/glview/glrenderer/GLPaint.java | 50 + .../glview/glrenderer/MovableTextTexture.java | 183 +++ .../glview/glrenderer/MultiLineTexture.java | 52 + .../glview/glrenderer/NativeTexture.java | 103 ++ .../glview/glrenderer/NinePatchChunk.java | 82 + .../glview/glrenderer/NinePatchTexture.java | 424 +++++ .../hippo/glview/glrenderer/RawTexture.java | 73 + .../glview/glrenderer/ResourceTexture.java | 53 + .../glview/glrenderer/SpriteTexture.java | 73 + .../glview/glrenderer/StringTexture.java | 87 ++ .../com/hippo/glview/glrenderer/Texture.java | 47 + .../glview/glrenderer/TextureUploader.java | 104 ++ .../hippo/glview/glrenderer/TiledTexture.java | 373 +++++ .../glview/glrenderer/UploadedTexture.java | 298 ++++ .../glview/glrenderer/XmlResourceTexture.java | 80 + .../glview/image/GLImageMovableTextView.java | 107 ++ .../glview/image/ImageMovableTextTexture.java | 211 +++ .../glview/image/ImageSpriteTexture.java | 91 ++ .../com/hippo/glview/image/ImageTexture.java | 705 +++++++++ .../com/hippo/glview/image/ImageWrapper.java | 192 +++ .../java/com/hippo/glview/util/ApiHelper.java | 234 +++ .../com/hippo/glview/util/GalleryUtils.java | 59 + .../hippo/glview/util/MotionEventHelper.java | 117 ++ .../com/hippo/glview/view/AnimationTime.java | 45 + .../java/com/hippo/glview/view/GLRoot.java | 55 + .../com/hippo/glview/view/GLRootView.java | 751 +++++++++ .../java/com/hippo/glview/view/GLView.java | 1367 +++++++++++++++++ .../java/com/hippo/glview/view/Gravity.java | 82 + .../hippo/glview/view/OrientationSource.java | 22 + .../com/hippo/glview/view/TouchHelper.java | 243 +++ .../com/hippo/glview/view/TouchOwner.java | 40 + .../com/hippo/glview/widget/GLEdgeEffect.java | 368 +++++ .../com/hippo/glview/widget/GLEdgeView.java | 163 ++ .../hippo/glview/widget/GLFrameLayout.java | 107 ++ .../hippo/glview/widget/GLLinearLayout.java | 243 +++ .../glview/widget/GLMovableTextView.java | 108 ++ .../hippo/glview/widget/GLProgressView.java | 242 +++ .../hippo/glview/widget/GLTextureView.java | 69 + 68 files changed, 16325 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/hippo/glgallery/DownUpDetector.java create mode 100644 app/src/main/java/com/hippo/glgallery/Fling.java create mode 100644 app/src/main/java/com/hippo/glgallery/GalleryPageView.java create mode 100644 app/src/main/java/com/hippo/glgallery/GalleryProvider.java create mode 100644 app/src/main/java/com/hippo/glgallery/GalleryView.java create mode 100644 app/src/main/java/com/hippo/glgallery/GestureRecognizer.java create mode 100644 app/src/main/java/com/hippo/glgallery/ImageView.java create mode 100644 app/src/main/java/com/hippo/glgallery/PagerLayoutManager.java create mode 100644 app/src/main/java/com/hippo/glgallery/ScrollLayoutManager.java create mode 100644 app/src/main/java/com/hippo/glgallery/SimpleAdapter.java create mode 100644 app/src/main/java/com/hippo/glview/anim/AlphaAnimation.java create mode 100644 app/src/main/java/com/hippo/glview/anim/Animation.java create mode 100644 app/src/main/java/com/hippo/glview/anim/CanvasAnimation.java create mode 100644 app/src/main/java/com/hippo/glview/anim/FloatAnimation.java create mode 100644 app/src/main/java/com/hippo/glview/annotation/RenderThread.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/BasicTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/BitmapTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/CanvasTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/ColorTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/ExtTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/FadeInTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/FadeOutTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/FadeTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLCanvas.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLES11Canvas.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLES11IdImpl.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLES20Canvas.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLES20IdImpl.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLId.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLPaint.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/MovableTextTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/MultiLineTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/NativeTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/NinePatchChunk.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/NinePatchTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/RawTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/ResourceTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/SpriteTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/StringTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/Texture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/TextureUploader.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/TiledTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/UploadedTexture.java create mode 100644 app/src/main/java/com/hippo/glview/glrenderer/XmlResourceTexture.java create mode 100644 app/src/main/java/com/hippo/glview/image/GLImageMovableTextView.java create mode 100644 app/src/main/java/com/hippo/glview/image/ImageMovableTextTexture.java create mode 100644 app/src/main/java/com/hippo/glview/image/ImageSpriteTexture.java create mode 100644 app/src/main/java/com/hippo/glview/image/ImageTexture.java create mode 100644 app/src/main/java/com/hippo/glview/image/ImageWrapper.java create mode 100644 app/src/main/java/com/hippo/glview/util/ApiHelper.java create mode 100644 app/src/main/java/com/hippo/glview/util/GalleryUtils.java create mode 100644 app/src/main/java/com/hippo/glview/util/MotionEventHelper.java create mode 100644 app/src/main/java/com/hippo/glview/view/AnimationTime.java create mode 100644 app/src/main/java/com/hippo/glview/view/GLRoot.java create mode 100644 app/src/main/java/com/hippo/glview/view/GLRootView.java create mode 100644 app/src/main/java/com/hippo/glview/view/GLView.java create mode 100644 app/src/main/java/com/hippo/glview/view/Gravity.java create mode 100644 app/src/main/java/com/hippo/glview/view/OrientationSource.java create mode 100644 app/src/main/java/com/hippo/glview/view/TouchHelper.java create mode 100644 app/src/main/java/com/hippo/glview/view/TouchOwner.java create mode 100644 app/src/main/java/com/hippo/glview/widget/GLEdgeEffect.java create mode 100644 app/src/main/java/com/hippo/glview/widget/GLEdgeView.java create mode 100644 app/src/main/java/com/hippo/glview/widget/GLFrameLayout.java create mode 100644 app/src/main/java/com/hippo/glview/widget/GLLinearLayout.java create mode 100644 app/src/main/java/com/hippo/glview/widget/GLMovableTextView.java create mode 100644 app/src/main/java/com/hippo/glview/widget/GLProgressView.java create mode 100644 app/src/main/java/com/hippo/glview/widget/GLTextureView.java diff --git a/app/build.gradle b/app/build.gradle index 9d25d05ff..f02c14d2a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -113,9 +113,6 @@ dependencies { implementation 'com.google.android.material:material:1.7.0-alpha02' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3' implementation 'com.github.seven332:beerbelly:0.1.4' - implementation 'com.github.seven332:glgallery:0.1.2' - implementation 'com.github.seven332:glview:0.1.0' - implementation 'com.github.seven332:glview-image:0.1.3' implementation 'com.github.seven332:streampipe:0.1.0' implementation 'com.github.seven332:tuxiang:0.1.6' implementation 'com.github.seven332:unifile:9ec57bcd8f' @@ -142,10 +139,6 @@ dependencies { configurations.all { resolutionStrategy { - force 'com.github.seven332:glgallery:25893283ca' - force 'com.github.seven332:glview:ba6aee61d7' - force 'com.github.seven332:glview-image:68d94b0fc2' - exclude group: 'com.github.seven332', module: 'okhttp' exclude group: 'com.github.seven332', module: 'image' exclude group: 'com.github.seven332', module: 'yorozuya' diff --git a/app/src/main/java/com/hippo/glgallery/DownUpDetector.java b/app/src/main/java/com/hippo/glgallery/DownUpDetector.java new file mode 100644 index 000000000..b54fb0a5d --- /dev/null +++ b/app/src/main/java/com/hippo/glgallery/DownUpDetector.java @@ -0,0 +1,67 @@ +/* + * 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.glgallery; + +import android.view.MotionEvent; + +class DownUpDetector { + public interface DownUpListener { + void onDown(MotionEvent e); + void onUp(MotionEvent e); + void onPointerDown(MotionEvent e); + void onPointerUp(MotionEvent e); + } + + private boolean mStillDown; + private final DownUpListener mListener; + + public DownUpDetector(DownUpListener listener) { + mListener = listener; + } + + private void setState(boolean down, MotionEvent e) { + if (down == mStillDown) return; + mStillDown = down; + if (down) { + mListener.onDown(e); + } else { + mListener.onUp(e); + } + } + + public void onTouchEvent(MotionEvent ev) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + setState(true, ev); + break; + case MotionEvent.ACTION_POINTER_DOWN: + mListener.onPointerDown(ev); + break; + case MotionEvent.ACTION_UP: + mListener.onPointerUp(ev); + setState(false, ev); + break; + case MotionEvent.ACTION_CANCEL: + setState(false, ev); + break; + } + } + + public boolean isDown() { + return mStillDown; + } +} diff --git a/app/src/main/java/com/hippo/glgallery/Fling.java b/app/src/main/java/com/hippo/glgallery/Fling.java new file mode 100644 index 000000000..01bfc2db0 --- /dev/null +++ b/app/src/main/java/com/hippo/glgallery/Fling.java @@ -0,0 +1,138 @@ +/* + * 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.glgallery; + +import android.content.Context; +import android.hardware.SensorManager; +import android.view.ViewConfiguration; +import android.view.animation.Interpolator; + +import com.hippo.glview.anim.Animation; + +abstract class Fling extends Animation { + + private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); + private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) + private static final float START_TENSION = 0.5f; + private static final float END_TENSION = 1.0f; + private static final float P1 = START_TENSION * INFLEXION; + private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION); + private static final float FLING_FRICTION = ViewConfiguration.getScrollFriction(); + + private static final int NB_SAMPLES = 100; + private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; + private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; + + private final float mPhysicalCoeff; + + static { + float x_min = 0.0f; + float y_min = 0.0f; + for (int i = 0; i < NB_SAMPLES; i++) { + final float alpha = (float) i / NB_SAMPLES; + + float x_max = 1.0f; + float x, tx, coef; + while (true) { + x = x_min + (x_max - x_min) / 2.0f; + coef = 3.0f * x * (1.0f - x); + tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x; + if (Math.abs(tx - alpha) < 1E-5) break; + if (tx > 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; + 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; + } + }; + + 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 + setInterpolator(FLING_INTERPOLATOR); + } + + private double getSplineDeceleration(int velocity) { + return Math.log(INFLEXION * Math.abs(velocity) / (FLING_FRICTION * mPhysicalCoeff)); + } + + /* Returns the duration, expressed in milliseconds */ + protected int getSplineFlingDuration(int velocity) { + final double l = getSplineDeceleration(velocity); + final double decelMinusOne = DECELERATION_RATE - 1.0; + return (int) (1000.0 * Math.exp(l / decelMinusOne)); + } + + protected 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. + **/ + protected 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; + } +} diff --git a/app/src/main/java/com/hippo/glgallery/GalleryPageView.java b/app/src/main/java/com/hippo/glgallery/GalleryPageView.java new file mode 100644 index 000000000..2b9376fce --- /dev/null +++ b/app/src/main/java/com/hippo/glgallery/GalleryPageView.java @@ -0,0 +1,197 @@ +/* + * 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.glgallery; + +import com.hippo.glview.glrenderer.BasicTexture; +import com.hippo.glview.glrenderer.Texture; +import com.hippo.glview.image.GLImageMovableTextView; +import com.hippo.glview.image.ImageMovableTextTexture; +import com.hippo.glview.image.ImageTexture; +import com.hippo.glview.view.Gravity; +import com.hippo.glview.widget.GLFrameLayout; +import com.hippo.glview.widget.GLLinearLayout; +import com.hippo.glview.widget.GLProgressView; +import com.hippo.glview.widget.GLTextureView; + +public class GalleryPageView extends GLFrameLayout { + + public static final int INVALID_INDEX = -1; + + public static final float PROGRESS_GONE = -1.0f; + public static final float PROGRESS_INDETERMINATE = -2.0f; + + private final ImageView mImage; + private final GLLinearLayout mInfo; + private final GLImageMovableTextView mPage; + private final GLTextureView mError; + private final GLProgressView mProgress; + + private final int mMinHeight; + + private int mIndex = INVALID_INDEX; + + public GalleryPageView(ImageMovableTextTexture pageTextTexture, + int progressColor, int progressBgColor, int progressSize, + int minHeight, int infoInterval) { + // Add image + mImage = new ImageView(); + GravityLayoutParams glp = new GravityLayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT); + addComponent(mImage, glp); + + // Add other panel + mInfo = new GLLinearLayout(); + mInfo.setOrientation(GLLinearLayout.VERTICAL); + mInfo.setInterval(infoInterval); + glp = new GravityLayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + glp.gravity = Gravity.CENTER; + addComponent(mInfo, glp); + + // Add page + mPage = new GLImageMovableTextView(); + mPage.setTextTexture(pageTextTexture); + GLLinearLayout.LayoutParams lp = new GLLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER_HORIZONTAL; + mInfo.addComponent(mPage, lp); + + // Add error + mError = new GLTextureView(); + lp = new GLLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER_HORIZONTAL; + mInfo.addComponent(mError, lp); + + // Add progress + mProgress = new GLProgressView(); + mProgress.setBgColor(progressBgColor); + mProgress.setColor(progressColor); + mProgress.setMinimumWidth(progressSize); + mProgress.setMinimumHeight(progressSize); + lp = new GLLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER_HORIZONTAL; + mInfo.addComponent(mProgress, lp); + + mMinHeight = minHeight; + } + + @Override + protected int getSuggestedMinimumHeight() { + // The height of the actual image may be smaller than mPageMinHeight. + // Set min height as 0 when the image is visible. + // For PageLayoutManager, min height is useless. + if (mImage.getVisibility() == VISIBLE) { + return 0; + } else { + return mMinHeight; + } + } + + int getIndex() { + return mIndex; + } + + void setIndex(int index) { + mIndex = index; + } + + public void showImage() { + mImage.setVisibility(VISIBLE); + mInfo.setVisibility(GONE); + } + + public void showInfo() { + // For image valid rect + mImage.setVisibility(INVISIBLE); + mInfo.setVisibility(VISIBLE); + } + + private void unbindImage() { + ImageTexture texture = mImage.getImageTexture(); + if (texture != null) { + mImage.setImageTexture(null); + texture.recycle(); + } + } + + public void setImage(ImageTexture imageTexture) { + unbindImage(); + if (imageTexture != null) { + mImage.setImageTexture(imageTexture); + } + } + + public void setPage(int page) { + mPage.setVisibility(VISIBLE); + mPage.setText(Integer.toString(page)); + } + + public void hidePage() { + mPage.setVisibility(GONE); + } + + public void setProgress(float progress) { + if (progress == PROGRESS_GONE) { + mProgress.setVisibility(GONE); + } else if (progress == PROGRESS_INDETERMINATE) { + mProgress.setVisibility(VISIBLE); + mProgress.setIndeterminate(true); + } else { + mProgress.setVisibility(VISIBLE); + mProgress.setIndeterminate(false); + mProgress.setProgress(progress); + } + } + + private void unbindError() { + Texture texture = mError.getTexture(); + if (texture != null) { + mError.setTexture(null); + if (texture instanceof BasicTexture) { + ((BasicTexture) texture).recycle(); + } + } + } + + public void setError(String error, GalleryView galleryView) { + unbindError(); + if (error == null) { + mError.setVisibility(GONE); + } else { + mError.setVisibility(VISIBLE); + galleryView.bindErrorView(mError, error); + } + } + + ImageView getImageView() { + return mImage; + } + + boolean isLoaded() { + return mImage.getVisibility() == VISIBLE; + } + + boolean isError() { + return mError.getVisibility() == VISIBLE; + } + + boolean isUnderInfo(float x, float y) { + return mInfo.bounds().contains((int) x, (int) y); + } +} diff --git a/app/src/main/java/com/hippo/glgallery/GalleryProvider.java b/app/src/main/java/com/hippo/glgallery/GalleryProvider.java new file mode 100644 index 000000000..c203a9ff2 --- /dev/null +++ b/app/src/main/java/com/hippo/glgallery/GalleryProvider.java @@ -0,0 +1,272 @@ +/* + * 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.glgallery; + +import android.util.LruCache; + +import androidx.annotation.IntDef; +import androidx.annotation.UiThread; + +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.glview.image.ImageWrapper; +import com.hippo.glview.view.GLRoot; +import com.hippo.image.Image; +import com.hippo.yorozuya.ConcurrentPool; +import com.hippo.yorozuya.MathUtils; +import com.hippo.yorozuya.OSUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public abstract class GalleryProvider { + + public static final int STATE_WAIT = -1; + public static final int STATE_ERROR = -2; + + private final ConcurrentPool mNotifyTaskPool = new ConcurrentPool<>(5); + private volatile Listener mListener; + private volatile GLRoot mGLRoot; + + private final ImageCache mImageCache = new ImageCache(); + + private boolean mStarted = false; + + @UiThread + public void start() { + OSUtils.checkMainLoop(); + + if (mStarted) { + throw new IllegalStateException("Can't start it twice"); + } + mStarted = true; + } + + @UiThread + public void stop() { + OSUtils.checkMainLoop(); + mImageCache.evictAll(); + } + + public void setGLRoot(GLRoot glRoot) { + mGLRoot = glRoot; + } + + /** + * @return {@link #STATE_WAIT} for wait, + * {@link #STATE_ERROR} for error, {@link #getError()} to get error message, + * 0 for empty + */ + public abstract int size(); + + public final void request(int index) { + ImageWrapper imageWrapper = mImageCache.get(index); + if (imageWrapper != null) { + notifyPageSucceed(index, imageWrapper); + } else { + onRequest(index); + } + } + + public final void forceRequest(int index) { + onForceRequest(index); + } + + public void removeCache(int index) { + mImageCache.remove(index); + } + + protected abstract void onRequest(int index); + + protected abstract void onForceRequest(int index); + + public final void cancelRequest(int index) { + onCancelRequest(index); + } + + protected abstract void onCancelRequest(int index); + + public abstract String getError(); + + public void setListener(Listener listener) { + mListener = listener; + } + + public void notifyDataChanged() { + notify(NotifyTask.TYPE_DATA_CHANGED, -1, 0.0f, null, null); + } + + public void notifyDataChanged(int index) { + notify(NotifyTask.TYPE_DATA_CHANGED, index, 0.0f, null, null); + } + + public void notifyPageWait(int index) { + notify(NotifyTask.TYPE_WAIT, index, 0.0f, null, null); + } + + public void notifyPagePercent(int index, float percent) { + notify(NotifyTask.TYPE_PERCENT, index, percent, null, null); + } + + public void notifyPageSucceed(int index, Image image) { + ImageWrapper imageWrapper = new ImageWrapper(image); + mImageCache.add(index, imageWrapper); + notifyPageSucceed(index, imageWrapper); + } + + public void notifyPageSucceed(int index, ImageWrapper image) { + notify(NotifyTask.TYPE_SUCCEED, index, 0.0f, image, null); + } + + public void notifyPageFailed(int index, String error) { + notify(NotifyTask.TYPE_FAILED, index, 0.0f, null, error); + } + + private void notify(@NotifyTask.Type int type, int index, float percent, ImageWrapper image, String error) { + Listener listener = mListener; + if (listener == null) { + return; + } + + GLRoot glRoot = mGLRoot; + if (glRoot == null) { + return; + } + + NotifyTask task = mNotifyTaskPool.pop(); + if (task == null) { + task = new NotifyTask(listener, mNotifyTaskPool); + } + task.setData(type, index, percent, image, error); + glRoot.addOnGLIdleListener(task); + } + + private static class NotifyTask implements GLRoot.OnGLIdleListener { + + @IntDef({TYPE_DATA_CHANGED, NotifyTask.TYPE_WAIT, TYPE_PERCENT, TYPE_SUCCEED, TYPE_FAILED}) + @Retention(RetentionPolicy.SOURCE) + public @interface Type {} + + public static final int TYPE_DATA_CHANGED = 0; + public static final int TYPE_WAIT = 1; + public static final int TYPE_PERCENT = 2; + public static final int TYPE_SUCCEED = 3; + public static final int TYPE_FAILED = 4; + + private final Listener mListener; + private final ConcurrentPool mPool; + + @Type + private int mType; + private int mIndex; + private float mPercent; + private ImageWrapper mImage; + private String mError; + + public NotifyTask(Listener listener, ConcurrentPool pool) { + mListener = listener; + mPool = pool; + } + + public void setData(@Type int type, int index, float percent, ImageWrapper image, String error) { + mType = type; + mIndex = index; + mPercent = percent; + mImage = image; + mError = error; + } + + @Override + public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { + switch (mType) { + case TYPE_DATA_CHANGED: + if (mIndex < 0) { + mListener.onDataChanged(); + } else { + mListener.onDataChanged(mIndex); + } + break; + case TYPE_WAIT: + mListener.onPageWait(mIndex); + break; + case TYPE_PERCENT: + mListener.onPagePercent(mIndex, mPercent); + break; + case TYPE_SUCCEED: + mListener.onPageSucceed(mIndex, mImage); + break; + case TYPE_FAILED: + mListener.onPageFailed(mIndex, mError); + break; + } + + // Clean data + mImage = null; + mError = null; + // Push back + mPool.push(this); + + return false; + } + } + + private static class ImageCache extends LruCache { + + private static final long MAX_CACHE_SIZE = 128 * 1024 * 1024; + private static final long MIN_CACHE_SIZE = 32 * 1024 * 1024; + + public ImageCache() { + super((int) MathUtils.clamp(OSUtils.getTotalMemory() / 16, MIN_CACHE_SIZE, MAX_CACHE_SIZE)); + } + + public void add(Integer key, ImageWrapper value) { + if (value.getFormat() != Image.FORMAT_GIF && value.obtain()) { + put(key, value); + } + } + + @Override + protected int sizeOf(Integer key, ImageWrapper value) { + int size = value.getWidth() * value.getHeight() * 4; + if (value.getFormat() == Image.FORMAT_GIF) { + size *= 5; + } + return size; + } + + @Override + protected void entryRemoved(boolean evicted, Integer key, ImageWrapper oldValue, ImageWrapper newValue) { + if (oldValue != null) { + oldValue.release(); + } + } + } + + public interface Listener { + + void onDataChanged(); + + void onPageWait(int index); + + void onPagePercent(int index, float percent); + + void onPageSucceed(int index, ImageWrapper image); + + void onPageFailed(int index, String error); + + void onDataChanged(int index); + } +} diff --git a/app/src/main/java/com/hippo/glgallery/GalleryView.java b/app/src/main/java/com/hippo/glgallery/GalleryView.java new file mode 100644 index 000000000..b00ce5864 --- /dev/null +++ b/app/src/main/java/com/hippo/glgallery/GalleryView.java @@ -0,0 +1,1238 @@ +/* + * 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.glgallery; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.view.MotionEvent; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.hippo.glview.annotation.RenderThread; +import com.hippo.glview.glrenderer.BasicTexture; +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.glview.glrenderer.StringTexture; +import com.hippo.glview.glrenderer.Texture; +import com.hippo.glview.image.ImageMovableTextTexture; +import com.hippo.glview.util.GalleryUtils; +import com.hippo.glview.view.AnimationTime; +import com.hippo.glview.view.GLRoot; +import com.hippo.glview.view.GLView; +import com.hippo.glview.widget.GLEdgeView; +import com.hippo.glview.widget.GLProgressView; +import com.hippo.glview.widget.GLTextureView; +import com.hippo.yorozuya.MathUtils; +import com.hippo.yorozuya.Pool; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public final class GalleryView extends GLView implements GestureRecognizer.Listener { + + @IntDef({LAYOUT_LEFT_TO_RIGHT, LAYOUT_RIGHT_TO_LEFT, LAYOUT_TOP_TO_BOTTOM}) + @Retention(RetentionPolicy.SOURCE) + public @interface LayoutMode {} + + @IntDef({SCALE_ORIGIN, SCALE_FIT_WIDTH, SCALE_FIT_HEIGHT, SCALE_FIT, SCALE_FIXED}) + @Retention(RetentionPolicy.SOURCE) + public @interface ScaleMode {} + + @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 LAYOUT_LEFT_TO_RIGHT = 0; + public static final int LAYOUT_RIGHT_TO_LEFT = 1; + public static final int LAYOUT_TOP_TO_BOTTOM = 2; + + public static final int SCALE_ORIGIN = ImageView.SCALE_ORIGIN; + public static final int SCALE_FIT_WIDTH = ImageView.SCALE_FIT_WIDTH; + public static final int SCALE_FIT_HEIGHT = ImageView.SCALE_FIT_HEIGHT; + public static final int SCALE_FIT = ImageView.SCALE_FIT; + public static final int SCALE_FIXED = ImageView.SCALE_FIXED; + + public static final int START_POSITION_TOP_LEFT = ImageView.START_POSITION_TOP_LEFT; + public static final int START_POSITION_TOP_RIGHT = ImageView.START_POSITION_TOP_RIGHT; + public static final int START_POSITION_BOTTOM_LEFT = ImageView.START_POSITION_BOTTOM_LEFT; + public static final int START_POSITION_BOTTOM_RIGHT = ImageView.START_POSITION_BOTTOM_RIGHT; + public static final int START_POSITION_CENTER = ImageView.START_POSITION_CENTER; + + 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[] MENU_AREA = {1.0f / 3.0f, 0.0f, 2.0f / 3.0f, 1.0f / 2.0f}; + private static final float[] SLIDER_AREA = {1.0f / 3.0f, 1.0f / 2.0f, 2.0f / 3.0f, 1.0f}; + + private static final int METHOD_ON_SINGLE_TAP_UP = 0; + private static final int METHOD_ON_SINGLE_TAP_CONFIRMED = 1; + private static final int METHOD_ON_DOUBLE_TAP = 2; + private static final int METHOD_ON_DOUBLE_TAP_CONFIRMED = 3; + private static final int METHOD_ON_LONG_PRESS = 4; + private static final int METHOD_ON_SCROLL = 5; + private static final int METHOD_ON_FLING = 6; + private static final int METHOD_ON_SCALE_BEGIN = 7; + private static final int METHOD_ON_SCALE = 8; + private static final int METHOD_ON_SCALE_END = 9; + private static final int METHOD_ON_DOWN = 10; + private static final int METHOD_ON_UP = 11; + private static final int METHOD_ON_POINTER_DOWN = 12; + private static final int METHOD_ON_POINTER_UP = 13; + private static final int METHOD_SET_LAYOUT_MODE = 14; + private static final int METHOD_SET_CURRENT_PAGE = 15; + private static final int METHOD_PAGE_LEFT = 16; + private static final int METHOD_PAGE_RIGHT = 17; + private static final int METHOD_SET_SCALE_MODE = 18; + private static final int METHOD_SET_START_POSITION = 19; + private static final int METHOD_ON_ATTACH_TO_ROOT = 20; + private static final int METHOD_SET_PAGER_INTERVAL = 21; + private static final int METHOD_SET_SCROLL_INTERVAL = 22; + + private final Context mContext; + private Adapter mAdapter; + private final GestureRecognizer mGestureRecognizer; + @Nullable + private final Listener mListener; + + private ImageMovableTextTexture mPageTextTexture; + + private PagerLayoutManager mPagerLayoutManager; + private ScrollLayoutManager mScrollLayoutManager; + @Nullable + private LayoutManager mLayoutManager; + + private final GLEdgeView mEdgeView; + private final Pool mGalleryPageViewPool = new Pool<>(5); + private GLProgressView mProgressCache; + private GLTextureView mErrorViewCache; + + private final int mBackgroundColor; + private int mPagerInterval; + private int mScrollInterval; + private final int mPageMinHeight; + private final int mPageInfoInterval; + private final int mProgressColor; + private final int mProgressSize; + private final int mPageTextColor; + private final int mPageTextSize; + private final Typeface mPageTextTypeface; + private final int mErrorTextSize; + private final int mErrorTextColor; + + private final String mDefaultErrorString; + private final String mEmptyString; + + private boolean mEnableRequestFill = true; + private boolean mRequestFill = false; + private boolean mWillFill = false; + + private boolean mScale = false; + private boolean mScroll = false; + private boolean mFirstScroll = false; + + private final Rect mLeftArea = new Rect(); + private final Rect mRightArea = new Rect(); + private final Rect mMenuArea = new Rect(); + private final Rect mSliderArea = new Rect(); + + private int mLayoutMode = LAYOUT_RIGHT_TO_LEFT; + private int mScaleMode = ImageView.SCALE_FIT; + private int mStartPosition = ImageView.START_POSITION_TOP_LEFT; + private int mIndex; + + private final List mMethodList = new ArrayList<>(5); + private final List mArgsList = new ArrayList<>(5); + private final List mMethodListTemp = new ArrayList<>(5); + private final List mArgsListTemp = new ArrayList<>(5); + + private final AtomicInteger mCurrentIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX); + + public static class Builder { + + private final Context mContext; + private final Adapter mAdapter; + private Listener mListener; + + private int mLayoutMode = LAYOUT_LEFT_TO_RIGHT; + private int mScaleMode = SCALE_FIT; + private int mStartPosition = START_POSITION_TOP_LEFT; + private int mStartPage = 0; + + private int mBackgroundColor = Color.BLACK; + private int mEdgeColor = Color.WHITE; + private int mPagerInterval = 48; + private int mScrollInterval = 24; + private int mPageMinHeight = 256; + private int mPageInfoInterval = 24; + private int mProgressColor = Color.WHITE; + private int mProgressSize = 56; + private int mPageTextColor = Color.WHITE; + private int mPageTextSize = 56; + private Typeface mPageTextTypeface = Typeface.DEFAULT; + private int mErrorTextColor = Color.RED; + private int mErrorTextSize = 24; + private String mDefaultErrorString = "Error"; + private String mEmptyString = "Empty"; + + public Builder(@NonNull Context context, @NonNull Adapter adapter) { + mContext = context; + mAdapter = adapter; + } + + public Builder setListener(Listener listener) { + mListener = listener; + return this; + } + + public Builder setLayoutMode(@LayoutMode int layoutMode) { + mLayoutMode = layoutMode; + return this; + } + + public Builder setScaleMode(@ScaleMode int scaleMode) { + mScaleMode = scaleMode; + return this; + } + + public Builder setStartPosition(@StartPosition int startPosition) { + mStartPosition = startPosition; + return this; + } + + public Builder setStartPage(int startPage) { + mStartPage = startPage; + return this; + } + + public Builder setBackgroundColor(int backgroundColor) { + mBackgroundColor = backgroundColor; + return this; + } + + public Builder setEdgeColor(int edgeColor) { + mEdgeColor = edgeColor; + return this; + } + + public Builder setPagerInterval(int pagerInterval) { + mPagerInterval = pagerInterval; + return this; + } + + public Builder setScrollInterval(int scrollInterval) { + mScrollInterval = scrollInterval; + return this; + } + + public Builder setPageMinHeight(int pageMinHeight) { + mPageMinHeight = pageMinHeight; + return this; + } + + public Builder setPageInfoInterval(int pageInfoInterval) { + mPageInfoInterval = pageInfoInterval; + return this; + } + + public Builder setProgressColor(int progressColor) { + mProgressColor = progressColor; + return this; + } + + public Builder setProgressSize(int progressSize) { + mProgressSize = progressSize; + return this; + } + + public Builder setPageTextColor(int pageTextColor) { + mPageTextColor = pageTextColor; + return this; + } + + public Builder setPageTextSize(int pageTextSize) { + mPageTextSize = pageTextSize; + return this; + } + + public Builder setPageTextTypeface(Typeface pageTextTypeface) { + mPageTextTypeface = pageTextTypeface; + return this; + } + + public Builder setErrorTextColor(int errorTextColor) { + mErrorTextColor = errorTextColor; + return this; + } + + public Builder setErrorTextSize(int errorTextSize) { + mErrorTextSize = errorTextSize; + return this; + } + + public Builder setDefaultErrorString(String defaultErrorString) { + mDefaultErrorString = defaultErrorString; + return this; + } + + public Builder setEmptyString(String emptyString) { + mEmptyString = emptyString; + return this; + } + + public GalleryView build() { + return new GalleryView(this); + } + } + + private GalleryView(Builder build) { + mContext = build.mContext; + mAdapter = build.mAdapter; + mAdapter.setGalleryView(this); + mListener = build.mListener; + mGestureRecognizer = new GestureRecognizer(mContext, this); + mEdgeView = new GLEdgeView(build.mEdgeColor); + + mLayoutMode = build.mLayoutMode; + mScaleMode = build.mScaleMode; + mStartPosition = build.mStartPosition; + mIndex = MathUtils.clamp(build.mStartPage, 0, Integer.MAX_VALUE); + + mBackgroundColor = build.mBackgroundColor; + mPageMinHeight = build.mPageMinHeight; + mPagerInterval = build.mPagerInterval; + mScrollInterval = build.mScrollInterval; + mPageInfoInterval = build.mPageInfoInterval; + mProgressColor = build.mProgressColor; + mProgressSize = build.mProgressSize; + mPageTextColor = build.mPageTextColor; + mPageTextSize = build.mPageTextSize; + mPageTextTypeface = build.mPageTextTypeface; + mErrorTextColor = build.mErrorTextColor; + mErrorTextSize = build.mErrorTextSize; + + mDefaultErrorString = build.mDefaultErrorString; + mEmptyString = build.mEmptyString; + + setBackgroundColor(mBackgroundColor); + } + + private void ensurePagerLayoutManager() { + if (mPagerLayoutManager == null) { + mPagerLayoutManager = new PagerLayoutManager(mContext, this, + mScaleMode, mStartPosition, 1.0f, mPagerInterval); + } + } + + private void ensureScrollLayoutManager() { + if (mScrollLayoutManager == null) { + mScrollLayoutManager = new ScrollLayoutManager(mContext, this, mScrollInterval); + } + } + + private void attachLayoutManager() { + if (null != mLayoutManager) { + return; + } + + switch (mLayoutMode) { + case LAYOUT_LEFT_TO_RIGHT: + ensurePagerLayoutManager(); + mPagerLayoutManager.setMode(PagerLayoutManager.MODE_LEFT_TO_RIGHT); + mPagerLayoutManager.onAttach(mAdapter); + mPagerLayoutManager.setCurrentIndex(mIndex); + mAdapter = null; + mLayoutManager = mPagerLayoutManager; + break; + case LAYOUT_RIGHT_TO_LEFT: + ensurePagerLayoutManager(); + mPagerLayoutManager.setMode(PagerLayoutManager.MODE_RIGHT_TO_LEFT); + mPagerLayoutManager.onAttach(mAdapter); + mPagerLayoutManager.setCurrentIndex(mIndex); + mAdapter = null; + mLayoutManager = mPagerLayoutManager; + break; + case LAYOUT_TOP_TO_BOTTOM: + ensureScrollLayoutManager(); + mScrollLayoutManager.onAttach(mAdapter); + mScrollLayoutManager.setCurrentIndex(mIndex); + mAdapter = null; + mLayoutManager = mScrollLayoutManager; + break; + } + + requestFill(); + } + + private void detachLayoutManager() { + if (null == mLayoutManager) { + return; + } + + mIndex = mLayoutManager.getInternalCurrentIndex(); + mAdapter = mLayoutManager.onDetach(); + mLayoutManager = null; + } + + @SuppressWarnings("deprecation") + private void onAttachToRootInternal() { + if (null == mPageTextTexture) { + mPageTextTexture = ImageMovableTextTexture.create(mPageTextTypeface, + mPageTextSize, mPageTextColor, + new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}); + } + attachLayoutManager(); + } + + private void setPagerIntervalInternal(int interval) { + mPagerInterval = interval; + if (mPagerLayoutManager != null) { + mPagerLayoutManager.setInterval(interval); + } + } + + private void setScrollIntervalInternal(int interval) { + mScrollInterval = interval; + if (mScrollLayoutManager != null) { + mScrollLayoutManager.setInterval(interval); + } + } + + @Override + public void onAttachToRoot(GLRoot root) { + super.onAttachToRoot(root); + mEdgeView.onAttachToRoot(root); + postMethod(METHOD_ON_ATTACH_TO_ROOT); + } + + @Override + public void onDetachFromRoot() { + // When detached, render() will not be called. So do it here + detachLayoutManager(); + if (null != mPageTextTexture) { + mPageTextTexture.recycle(); + mPageTextTexture = null; + } + + super.onDetachFromRoot(); + mEdgeView.onDetachFromRoot(); + } + + @LayoutMode + public static int sanitizeLayoutMode(int layoutMode) { + if (layoutMode != GalleryView.LAYOUT_LEFT_TO_RIGHT && + layoutMode != GalleryView.LAYOUT_RIGHT_TO_LEFT && + layoutMode != GalleryView.LAYOUT_TOP_TO_BOTTOM) { + return GalleryView.LAYOUT_LEFT_TO_RIGHT; + } else { + return layoutMode; + } + } + + @ScaleMode + public static int sanitizeScaleMode(int scaleMode) { + if (scaleMode != GalleryView.SCALE_ORIGIN && + scaleMode != GalleryView.SCALE_FIT_WIDTH && + scaleMode != GalleryView.SCALE_FIT_HEIGHT && + scaleMode != GalleryView.SCALE_FIT && + scaleMode != GalleryView.SCALE_FIXED) { + return GalleryView.SCALE_FIT; + } else { + return scaleMode; + } + } + + @StartPosition + public static int sanitizeStartPosition(int startPosition) { + if (startPosition != GalleryView.START_POSITION_TOP_LEFT && + startPosition != GalleryView.START_POSITION_TOP_RIGHT && + startPosition != GalleryView.START_POSITION_BOTTOM_LEFT && + startPosition != GalleryView.START_POSITION_BOTTOM_RIGHT && + startPosition != GalleryView.START_POSITION_CENTER) { + return GalleryView.START_POSITION_TOP_LEFT; + } else { + return startPosition; + } + } + + public int getLayoutMode() { + return mLayoutMode; + } + + public int getCurrentIndex() { + return mCurrentIndex.get(); + } + + @Override + public void requestLayout() { + // Do not need requestLayout, because the size will not change + requestFill(); + } + + public void requestFill() { + if (mEnableRequestFill) { + mRequestFill = true; + if (!mWillFill) { + invalidate(); + } + } + } + + @Override + protected boolean dispatchTouchEvent(MotionEvent event) { + // Do not pass event to component, so handle event here + mGestureRecognizer.onTouchEvent(event); + return true; + } + + GLEdgeView getEdgeView() { + return mEdgeView; + } + + String getDefaultErrorStr() { + return mDefaultErrorString; + } + + String getEmptyStr() { + return mEmptyString; + } + + boolean isFirstScroll() { + boolean firstScroll = mFirstScroll; + mFirstScroll = false; + return firstScroll; + } + + // Make sure method run in render thread to ensure thread safe + private void postMethod(int method, Object... args) { + synchronized (this) { + mMethodList.add(method); + mArgsList.add(args); + } + + invalidate(); + } + + public void setLayoutMode(@LayoutMode int layoutMode) { + postMethod(METHOD_SET_LAYOUT_MODE, layoutMode); + } + + public void setCurrentPage(int page) { + postMethod(METHOD_SET_CURRENT_PAGE, page); + } + + public void pageLeft() { + postMethod(METHOD_PAGE_LEFT); + } + + public void pageRight() { + postMethod(METHOD_PAGE_RIGHT); + } + + public void setScaleMode(int scaleMode) { + postMethod(METHOD_SET_SCALE_MODE, scaleMode); + } + + public void setStartPosition(int startPosition) { + postMethod(METHOD_SET_START_POSITION, startPosition); + } + + public void setPagerInterval(int interval) { + postMethod(METHOD_SET_PAGER_INTERVAL, interval); + } + + public void setScrollInterval(int interval) { + postMethod(METHOD_SET_SCROLL_INTERVAL, interval); + } + + @Override + public boolean onSingleTapUp(float x, float y) { + postMethod(METHOD_ON_SINGLE_TAP_UP, x, y); + return true; + } + + @Override + public boolean onSingleTapConfirmed(float x, float y) { + postMethod(METHOD_ON_SINGLE_TAP_CONFIRMED, x, y); + return true; + } + + @Override + public boolean onDoubleTap(float x, float y) { + postMethod(METHOD_ON_DOUBLE_TAP, x, y); + return true; + } + + @Override + public boolean onDoubleTapConfirmed(float x, float y) { + postMethod(METHOD_ON_DOUBLE_TAP_CONFIRMED, x, y); + return true; + } + + @Override + public void onLongPress(float x, float y) { + if (mLayoutManager != null && !mLayoutManager.isTapOrPressEnable()) { + return; + } + + postMethod(METHOD_ON_LONG_PRESS, x, y); + } + + @Override + public boolean onScroll(float dx, float dy, float totalX, float totalY, float x, float y) { + postMethod(METHOD_ON_SCROLL, dx, dy, totalX, totalY, x, y); + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + postMethod(METHOD_ON_FLING, velocityX, velocityY); + return true; + } + + @Override + public boolean onScaleBegin(float focusX, float focusY) { + postMethod(METHOD_ON_SCALE_BEGIN, focusX, focusY); + return true; + } + + @Override + public boolean onScale(float focusX, float focusY, float scale) { + postMethod(METHOD_ON_SCALE, focusX, focusY, scale); + return true; + } + + @Override + public void onScaleEnd() { + postMethod(METHOD_ON_SCALE_END); + } + + @Override + public void onDown(float x, float y) { + postMethod(METHOD_ON_DOWN, x, y); + } + + @Override + public void onUp() { + postMethod(METHOD_ON_UP); + } + + @Override + public void onPointerDown(float x, float y) { + postMethod(METHOD_ON_POINTER_DOWN, x, y); + } + + @Override + public void onPointerUp() { + postMethod(METHOD_ON_POINTER_UP); + } + + @Override + protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) { + mEdgeView.layout(left, top, right, bottom); + + 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)); + mMenuArea.set((int) (MENU_AREA[0] * width), (int) (MENU_AREA[1] * height), + (int) (MENU_AREA[2] * width), (int) (MENU_AREA[3] * height)); + mSliderArea.set((int) (SLIDER_AREA[0] * width), (int) (SLIDER_AREA[1] * height), + (int) (SLIDER_AREA[2] * width), (int) (SLIDER_AREA[3] * height)); + } + } + + @RenderThread + public void onDataChanged() { + GalleryUtils.assertInRenderThread(); + + if (mLayoutManager != null){ + mLayoutManager.onDataChanged(); + } + } + + private void onSingleTapUpInternal(float x, float y) { + } + + private GalleryPageView findPageUnder(float x, float y) { + for (int i = 0, n = getComponentCount(); i < n; i++) { + GLView view = getComponent(i); + if (view instanceof GalleryPageView && view.bounds().contains((int) x, (int) y)) { + return (GalleryPageView) view; + } + } + return null; + } + + private void onSingleTapConfirmedInternal(float x, float y) { + if (mLayoutManager == null || !mLayoutManager.isTapOrPressEnable()) { + return; + } + + GalleryPageView page = findPageUnder(x, y); + if (page != null && + page.getIndex() != GalleryPageView.INVALID_INDEX && + page.isError() && + page.isUnderInfo(x - page.bounds().left, y - page.bounds().top)) { + if (mListener != null) { + mListener.onTapErrorText(page.getIndex()); + } + } else if (mSliderArea.contains((int) x, (int) y)) { + if (mListener != null) { + mListener.onTapSliderArea(); + } + } else if (mMenuArea.contains((int) x, (int) y)) { + if (mListener != null) { + mListener.onTapMenuArea(); + } + } else if (mLeftArea.contains((int) x, (int) y)) { + mLayoutManager.onPageLeft(); + } else if (mRightArea.contains((int) x, (int) y)) { + mLayoutManager.onPageRight(); + } + } + + private void onDoubleTapInternal(float x, float y) { + } + + private void onDoubleTapConfirmedInternal(float x, float y) { + if (mScale) { + return; + } + + if (mLayoutManager != null) { + mLayoutManager.onDoubleTapConfirmed(x, y); + } + } + + private void onLongPressInternal(float x, float y) { + if (mScale) { + return; + } + + if (mLayoutManager == null) { + return; + } + + int index = mLayoutManager.getIndexUnder(x, y); + if (index == GalleryPageView.INVALID_INDEX) { + return; + } + + if (mListener != null) { + mListener.onLongPressPage(index); + } + } + + private void onScrollInternal(float dx, float dy, float totalX, float totalY, float x, float y) { + if (mScale) { + return; + } + mScroll = true; + + if (mLayoutManager != null) { + mLayoutManager.onScroll(dx, dy, totalX, totalY, x, y); + } + } + + private void onFlingInternal(float velocityX, float velocityY) { + if (mLayoutManager != null) { + mLayoutManager.onFling(velocityX, velocityY); + } + } + + private void onScaleBeginInternal(float focusX, float focusY) { + onScaleInternal(focusX, focusY, 1.0f); + } + + private void onScaleInternal(float focusX, float focusY, float scale) { + if (mScroll || (mLayoutManager != null && !mLayoutManager.canScale())) { + return; + } + mScale = true; + + if (mLayoutManager != null) { + mLayoutManager.onScale(focusX, focusY, scale); + } + } + + private void onScaleEndInternal() { + } + + private void onDownInternal(float x, float y) { + mScale = false; + mScroll = false; + mFirstScroll = true; + if (mLayoutManager != null) { + mLayoutManager.onDown(); + } + } + + private void onUpInternal() { + if (mLayoutManager != null) { + mLayoutManager.onUp(); + } + } + + private void onPointerDownInternal(float x, float y) { + if (!mScroll && (mLayoutManager != null && mLayoutManager.canScale())) { + mScale = true; + } + } + + private void onPointerUpInternal() { + } + + private void setLayoutModeInternal(int layoutMode) { + if (mLayoutMode == layoutMode) { + return; + } + mLayoutMode = layoutMode; + + if (mLayoutManager == null) { + return; + } + + switch (mLayoutMode) { + case LAYOUT_LEFT_TO_RIGHT: + if (mLayoutManager == mPagerLayoutManager) { + // mPagerLayoutManager already attached, just change mode + mPagerLayoutManager.setMode(PagerLayoutManager.MODE_LEFT_TO_RIGHT); + } else { + ensurePagerLayoutManager(); + mPagerLayoutManager.setMode(PagerLayoutManager.MODE_LEFT_TO_RIGHT); + int index = mLayoutManager.getInternalCurrentIndex(); + mPagerLayoutManager.onAttach(mLayoutManager.onDetach()); + mPagerLayoutManager.setCurrentIndex(index); + mLayoutManager = mPagerLayoutManager; + } + break; + case LAYOUT_RIGHT_TO_LEFT: + if (mLayoutManager == mPagerLayoutManager) { + // mPagerLayoutManager already attached, just change mode + mPagerLayoutManager.setMode(PagerLayoutManager.MODE_RIGHT_TO_LEFT); + } else { + ensurePagerLayoutManager(); + mPagerLayoutManager.setMode(PagerLayoutManager.MODE_RIGHT_TO_LEFT); + int index = mLayoutManager.getInternalCurrentIndex(); + mPagerLayoutManager.onAttach(mLayoutManager.onDetach()); + mPagerLayoutManager.setCurrentIndex(index); + mLayoutManager = mPagerLayoutManager; + } + break; + case LAYOUT_TOP_TO_BOTTOM: + ensureScrollLayoutManager(); + int index = mLayoutManager.getInternalCurrentIndex(); + mScrollLayoutManager.onAttach(mLayoutManager.onDetach()); + mScrollLayoutManager.setCurrentIndex(index); + mLayoutManager = mScrollLayoutManager; + break; + } + + requestFill(); + } + + private void setCurrentPageInternal(int page) { + if (mLayoutManager != null) { + mLayoutManager.setCurrentIndex(page); + } else { + mIndex = page; + } + } + + private void pageLeftInternal() { + if (mLayoutManager != null) { + mLayoutManager.onPageLeft(); + } + } + + private void pageRightInternal() { + if (mLayoutManager != null) { + mLayoutManager.onPageRight(); + } + } + + private void setScaleModeInternal(int scaleMode) { + mScaleMode = scaleMode; + if (mPagerLayoutManager != null) { + mPagerLayoutManager.setScaleMode(scaleMode); + } + } + + private void setStartPositionInternal(int startPosition) { + mStartPosition = startPosition; + if (mPagerLayoutManager != null) { + mPagerLayoutManager.setStartPosition(startPosition); + } + } + + @RenderThread + void forceFill() { + mRequestFill = true; + fill(); + } + + @RenderThread + private void fill() { + GalleryUtils.assertInRenderThread(); + + if (!mRequestFill) { + return; + } + + // Disable request layout + mEnableRequestFill = false; + if (mLayoutManager != null) { + mLayoutManager.onFill(); + } + mEnableRequestFill = true; + mRequestFill = false; + } + + private void dispatchMethod() { + List methodListTemp = mMethodListTemp; + List argsListTemp = mArgsListTemp; + + synchronized (this) { + if (mMethodList.isEmpty()) { + return; + } + + methodListTemp.addAll(mMethodList); + argsListTemp.addAll(mArgsList); + mMethodList.clear(); + mArgsList.clear(); + } + + for (int i = 0, n = methodListTemp.size(); i < n; i++) { + int method = methodListTemp.get(i); + Object[] args = argsListTemp.get(i); + + switch (method) { + case METHOD_ON_SINGLE_TAP_UP: + onSingleTapUpInternal((Float) args[0], (Float) args[1]); + break; + case METHOD_ON_SINGLE_TAP_CONFIRMED: + onSingleTapConfirmedInternal((Float) args[0], (Float) args[1]); + break; + case METHOD_ON_DOUBLE_TAP: + onDoubleTapInternal((Float) args[0], (Float) args[1]); + break; + case METHOD_ON_DOUBLE_TAP_CONFIRMED: + onDoubleTapConfirmedInternal((Float) args[0], (Float) args[1]); + break; + case METHOD_ON_LONG_PRESS: + onLongPressInternal((Float) args[0], (Float) args[1]); + break; + case METHOD_ON_SCROLL: + onScrollInternal((Float) args[0], (Float) args[1], (Float) args[2], + (Float) args[3], (Float) args[4], (Float) args[5]); + break; + case METHOD_ON_FLING: + onFlingInternal((Float) args[0], (Float) args[1]); + break; + case METHOD_ON_SCALE_BEGIN: + onScaleBeginInternal((Float) args[0], (Float) args[1]); + break; + case METHOD_ON_SCALE: + onScaleInternal((Float) args[0], (Float) args[1], (Float) args[2]); + break; + case METHOD_ON_SCALE_END: + onScaleEndInternal(); + break; + case METHOD_ON_DOWN: + onDownInternal((Float) args[0], (Float) args[1]); + break; + case METHOD_ON_UP: + onUpInternal(); + break; + case METHOD_ON_POINTER_DOWN: + onPointerDownInternal((Float) args[0], (Float) args[1]); + break; + case METHOD_ON_POINTER_UP: + onPointerUpInternal(); + break; + case METHOD_SET_LAYOUT_MODE: + setLayoutModeInternal((Integer) args[0]); + break; + case METHOD_SET_CURRENT_PAGE: + setCurrentPageInternal((Integer) args[0]); + break; + case METHOD_PAGE_LEFT: + pageLeftInternal(); + break; + case METHOD_PAGE_RIGHT: + pageRightInternal(); + break; + case METHOD_SET_SCALE_MODE: + setScaleModeInternal((Integer) args[0]); + break; + case METHOD_SET_START_POSITION: + setStartPositionInternal((Integer) args[0]); + break; + case METHOD_ON_ATTACH_TO_ROOT: + onAttachToRootInternal(); + break; + case METHOD_SET_PAGER_INTERVAL: + setPagerIntervalInternal((Integer) args[0]); + break; + case METHOD_SET_SCROLL_INTERVAL: + setScrollIntervalInternal((Integer) args[0]); + break; + } + } + + methodListTemp.clear(); + argsListTemp.clear(); + } + + @Override + public void render(GLCanvas canvas) { + mWillFill = true; + int oldCurrentIndex = mCurrentIndex.get(); + + // Dispatch method + dispatchMethod(); + + long time = AnimationTime.get(); + if (mLayoutManager != null && mLayoutManager.onUpdateAnimation(time)) { + invalidate(); + } + + fill(); + mWillFill = false; + + super.render(canvas); + mEdgeView.render(canvas); + + int newCurrentIndex; + if (mLayoutManager != null) { + newCurrentIndex = mLayoutManager.getCurrentIndex(); + } else { + newCurrentIndex = GalleryPageView.INVALID_INDEX; + } + mCurrentIndex.lazySet(newCurrentIndex); + + if (oldCurrentIndex != newCurrentIndex && mListener != null) { + mListener.onUpdateCurrentIndex(newCurrentIndex); + } + } + + @RenderThread + public GalleryPageView findPageByIndex(int id) { + if (mLayoutManager != null) { + return mLayoutManager.findPageByIndex(id); + } else { + return null; + } + } + + GalleryPageView obtainPage() { + GalleryPageView page = mGalleryPageViewPool.pop(); + if (page == null) { + page = new GalleryPageView(mPageTextTexture, + mProgressColor, mBackgroundColor, mProgressSize, + mPageMinHeight, mPageInfoInterval); + } + return page; + } + + void releasePage(GalleryPageView page) { + mGalleryPageViewPool.push(page); + } + + /** + * Indeterminate GLProgressView + */ + GLProgressView obtainProgress() { + GLProgressView progress; + if (mProgressCache != null) { + progress = mProgressCache; + mProgressCache = null; + } else { + progress = new GLProgressView(); + progress.setColor(mProgressColor); + progress.setBgColor(mBackgroundColor); + progress.setIndeterminate(true); + progress.setMinimumWidth(mProgressSize); + progress.setMinimumHeight(mProgressSize); + } + return progress; + } + + /** + * @param progress Indeterminate GLProgressView + */ + void releaseProgress(GLProgressView progress) { + mProgressCache = progress; + } + + GLTextureView obtainErrorView() { + GLTextureView errorView; + if (mErrorViewCache != null) { + errorView = mErrorViewCache; + mErrorViewCache = null; + } else { + errorView = new GLTextureView(); + } + return errorView; + } + + void unbindErrorView(GLTextureView errorView) { + Texture texture = errorView.getTexture(); + if (texture != null) { + errorView.setTexture(null); + if (texture instanceof BasicTexture) { + ((BasicTexture) texture).recycle(); + } + } + } + + void bindErrorView(GLTextureView errorView, String error) { + unbindErrorView(errorView); + + Texture texture = StringTexture.newInstance(error, mErrorTextSize, mErrorTextColor); + errorView.setTexture(texture); + } + + void releaseErrorView(GLTextureView errorView) { + unbindErrorView(errorView); + mErrorViewCache = errorView; + } + + public static abstract class Adapter { + + protected GalleryView mGalleryView; + + private void setGalleryView(@NonNull GalleryView galleryView) { + mGalleryView = galleryView; + } + + public void bind(GalleryPageView view, int index) { + onBind(view, index); + view.setIndex(index); + } + + public void unbind(GalleryPageView view) { + onUnbind(view, view.getIndex()); + view.setIndex(GalleryPageView.INVALID_INDEX); + } + + public abstract void onBind(GalleryPageView view, int index); + + public abstract void onUnbind(GalleryPageView view, int index); + + /** + * @return Null for no error + */ + public abstract String getError(); + + public abstract int size(); + } + + public static abstract class LayoutManager { + + protected GalleryView mGalleryView; + + public LayoutManager(@NonNull GalleryView galleryView) { + mGalleryView = galleryView; + } + + public abstract void onAttach(Adapter iterator); + + public abstract Adapter onDetach(); + + public abstract void onFill(); + + public abstract void onDown(); + + public abstract void onUp(); + + public abstract void onDoubleTapConfirmed(float x, float y); + + public abstract void onLongPress(float x, float y); + + public abstract void onScroll(float dx, float dy, float totalX, float totalY, float x, float y); + + public abstract void onFling(float velocityX, float velocityY); + + public abstract boolean canScale(); + + public abstract void onScale(float focusX, float focusY, float scale); + + public abstract boolean onUpdateAnimation(long time); + + public abstract void onDataChanged(); + + public abstract void onPageLeft(); + + public abstract void onPageRight(); + + public abstract boolean isTapOrPressEnable(); + + public abstract GalleryPageView findPageByIndex(int index); + + /** + * @return {@link GalleryPageView#INVALID_INDEX} for error + */ + public abstract int getCurrentIndex(); + + public abstract void setCurrentIndex(int index); + + public abstract int getIndexUnder(float x, float y); + + abstract int getInternalCurrentIndex(); + + protected void placeCenter(GLView view) { + int spec = GLView.MeasureSpec.makeMeasureSpec(GLView.LayoutParams.WRAP_CONTENT, + GLView.LayoutParams.WRAP_CONTENT); + view.measure(spec, spec); + int viewWidth = view.getMeasuredWidth(); + int viewHeight = view.getMeasuredHeight(); + int viewLeft = mGalleryView.getWidth() / 2 - viewWidth / 2; + int viewTop = mGalleryView.getHeight() / 2 - viewHeight / 2; + view.layout(viewLeft, viewTop, viewLeft + viewWidth, viewTop + viewHeight); + } + } + + public interface Listener { + + @RenderThread + void onUpdateCurrentIndex(int index); + + @RenderThread + void onTapSliderArea(); + + @RenderThread + void onTapMenuArea(); + + @RenderThread + void onTapErrorText(int index); + + @RenderThread + void onLongPressPage(int index); + } +} diff --git a/app/src/main/java/com/hippo/glgallery/GestureRecognizer.java b/app/src/main/java/com/hippo/glgallery/GestureRecognizer.java new file mode 100644 index 000000000..50d73b65d --- /dev/null +++ b/app/src/main/java/com/hippo/glgallery/GestureRecognizer.java @@ -0,0 +1,175 @@ +/* + * 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.glgallery; + +import android.content.Context; +import android.os.SystemClock; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +import androidx.core.view.GestureDetectorCompat; + +// This class aggregates three gesture detectors: GestureDetector, +// ScaleGestureDetector, and DownUpDetector. +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); + void onLongPress(float x, float y); + boolean onScroll(float dx, float dy, float totalX, float totalY, float x, float y); + + /** + * @param velocityX Finger from top to bottom is positive + * @param velocityY Finger from left to right is positive + */ + 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 void onLongPress(MotionEvent e) { + mListener.onLongPress(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/glgallery/ImageView.java b/app/src/main/java/com/hippo/glgallery/ImageView.java new file mode 100644 index 000000000..cd0669ad7 --- /dev/null +++ b/app/src/main/java/com/hippo/glgallery/ImageView.java @@ -0,0 +1,604 @@ +/* + * 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.glgallery; + +import android.graphics.Rect; +import android.graphics.RectF; +import com.hippo.glview.anim.AlphaAnimation; +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.glview.glrenderer.Texture; +import com.hippo.glview.image.ImageTexture; +import com.hippo.glview.view.GLView; +import com.hippo.yorozuya.AnimationUtils; +import com.hippo.yorozuya.MathUtils; +import java.util.Arrays; + +class ImageView extends GLView implements ImageTexture.Callback { + + // TODO adjust scale max and min according 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 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 SCALE_FIXED = 4; + + 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 static final long ALPHA_ANIMATION_DURING = 300L; + + private ImageTexture mImageTexture; + private int mTextureWidth; + private int mTextureHeight; + + private final RectF mDst = new RectF(); + private final RectF mSrcActual = new RectF(); + private final RectF mDstActual = new RectF(); + private final Rect mValidRect = new Rect(); + + private int mScaleMode = SCALE_FIT; + private int mStartPosition = START_POSITION_TOP_RIGHT; + private float mScaleValue = 1.0f; + + private float mScale = 1.0f; + + private boolean mScaleOffsetDirty = true; + private boolean mPositionInRootDirty = true; + + private final AlphaAnimation mAlphaAnimation; + + public ImageView() { + mAlphaAnimation = new AlphaAnimation(0.0f, 1.0f); + mAlphaAnimation.setDuration(ALPHA_ANIMATION_DURING); + mAlphaAnimation.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR); + } + + public static int sanitizeScaleMode(int scaleMode) { + if (scaleMode != SCALE_ORIGIN && + scaleMode != SCALE_FIT_WIDTH && + scaleMode != SCALE_FIT_HEIGHT && + scaleMode != SCALE_FIT && + scaleMode != SCALE_FIXED) { + return SCALE_FIT; + } else { + return scaleMode; + } + } + + public static int sanitizeStartPosition(int startPosition) { + if (startPosition != START_POSITION_TOP_LEFT && + startPosition != START_POSITION_TOP_RIGHT && + startPosition != START_POSITION_BOTTOM_LEFT && + startPosition != START_POSITION_BOTTOM_RIGHT && + startPosition != START_POSITION_CENTER) { + return START_POSITION_TOP_RIGHT; + } else { + return startPosition; + } + } + + @Override + protected int getSuggestedMinimumWidth() { + return Math.max(super.getSuggestedMinimumWidth(), + mImageTexture == null ? 0 : mTextureWidth); + } + + @Override + protected int getSuggestedMinimumHeight() { + return Math.max(super.getSuggestedMinimumHeight(), + mImageTexture == null ? 0 : mTextureHeight); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + if (mImageTexture == null) { + super.onMeasure(widthSpec, heightSpec); + } else { + float ratio = (float) mTextureWidth / mTextureHeight; + int widthSize = MeasureSpec.getSize(widthSpec); + int heightSize = MeasureSpec.getSize(heightSpec); + int widthMode = MeasureSpec.getMode(widthSpec); + int heightMode = MeasureSpec.getMode(heightSpec); + int measureWidth = -1; + int measureHeight = -1; + + if (widthMode == MeasureSpec.EXACTLY) { + measureWidth = widthSize; + if (heightMode == MeasureSpec.EXACTLY) { + measureHeight = heightSize; + } else { + measureHeight = (int) (widthSize / ratio); + if (heightMode == MeasureSpec.AT_MOST) { + measureHeight = Math.min(measureHeight, heightSize); + } + } + } else if (heightMode == MeasureSpec.EXACTLY) { + measureHeight = heightSize; + measureWidth = (int) (heightSize * ratio); + if (widthMode == MeasureSpec.AT_MOST) { + measureWidth = Math.min(measureWidth, widthSize); + } + } + + if (measureWidth == -1 || measureHeight == -1) { + super.onMeasure(widthSpec, heightSpec); + } else { + setMeasuredSize(measureWidth, measureHeight); + } + } + } + + @Override + protected void onSizeChanged(int newW, int newH, int oldW, int oldH) { + mScaleOffsetDirty = true; + mPositionInRootDirty = true; + } + + @Override + protected void onPositionInRootChanged(int x, int y, int oldX, int oldY) { + mPositionInRootDirty = true; + + if (mImageTexture != null) { + getValidRect(mValidRect); + if (!mValidRect.isEmpty()) { + mImageTexture.start(); + } else { + mImageTexture.stop(); + } + } + } + + public void getScaleDefault(float[] scaleDefault) { + if (mImageTexture == null) { + return; + } + + scaleDefault[0] = 1.0f; + scaleDefault[1] = (float) getWidth() / mTextureWidth; + scaleDefault[2] = (float) getHeight() / mTextureHeight; + scaleDefault[3] = Math.max(scaleDefault[1], scaleDefault[2]) * 2; + + scaleDefault[0] = MathUtils.clamp(scaleDefault[0], SCALE_MIN, SCALE_MAX); + scaleDefault[1] = MathUtils.clamp(scaleDefault[1], SCALE_MIN, SCALE_MAX); + scaleDefault[2] = MathUtils.clamp(scaleDefault[2], SCALE_MIN, SCALE_MAX); + scaleDefault[3] = MathUtils.clamp(scaleDefault[3], SCALE_MIN, SCALE_MAX); + + Arrays.sort(scaleDefault); + } + + public void setImageTexture(ImageTexture imageTexture) { + // Remove callback + if (mImageTexture != null) { + mImageTexture.setCallback(null); + mImageTexture.stop(); + } + + int oldTextureWidth = mTextureWidth; + int oldTextureHeight = mTextureHeight; + + mImageTexture = imageTexture; + + if (imageTexture != null) { + imageTexture.setCallback(this); + mTextureWidth = imageTexture.getWidth(); + mTextureHeight = imageTexture.getHeight(); + // Avoid zero and negative + if (mTextureWidth <= 0) { + mTextureWidth = 1; + } + if (mTextureHeight <= 0) { + mTextureHeight = 1; + } + + // Start alpha animation, do not show animation for image has no valid rect + getValidRect(mValidRect); + if (!mValidRect.isEmpty()) { + startAnimation(mAlphaAnimation, true); + mImageTexture.start(); + } + } else { + mTextureWidth = 1; + mTextureHeight = 1; + } + + mScaleOffsetDirty = true; + mPositionInRootDirty = true; + + if (oldTextureWidth != mTextureWidth || oldTextureHeight != mTextureHeight) { + requestLayout(); + } + } + + public ImageTexture getImageTexture() { + return mImageTexture; + } + + public boolean isLoaded() { + return mImageTexture != null; + } + + public boolean canFlingVertically() { + if (mScaleOffsetDirty) { + setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + if (mScaleOffsetDirty) { + return false; + } + } + + return mDst.top < 0.0f || mDst.bottom > getHeight(); + } + + public boolean canFlingHorizontally() { + if (mScaleOffsetDirty) { + setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + if (mScaleOffsetDirty) { + return false; + } + } + + return mDst.left < 0.0f || mDst.right > getWidth(); + } + + public boolean canFling() { + if (mScaleOffsetDirty) { + setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + if (mScaleOffsetDirty) { + return false; + } + } + + return mDst.left < 0.0f || mDst.top < 0.0f || mDst.right > getWidth() || mDst.bottom > getHeight(); + } + + public int getMaxDx() { + if (mScaleOffsetDirty) { + setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + if (mScaleOffsetDirty) { + return 0; + } + } + return Math.max(0, -(int) mDst.left); + } + + public int getMinDx() { + if (mScaleOffsetDirty) { + setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + if (mScaleOffsetDirty) { + return 0; + } + } + return Math.min(0, getWidth() - (int) mDst.right); + } + + public int getMaxDy() { + if (mScaleOffsetDirty) { + setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + if (mScaleOffsetDirty) { + return 0; + } + } + return Math.max(0, -(int) mDst.top); + } + + public int getMinDy() { + if (mScaleOffsetDirty) { + setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + if (mScaleOffsetDirty) { + return 0; + } + } + return Math.min(0, getHeight() - (int) mDst.bottom); + } + + public float getScale() { + return mScale; + } + + /** + * 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 dst = mDst; + int screenWidth = getWidth(); + int screenHeight = getHeight(); + float targetWidth = dst.width(); + float targetHeight = dst.height(); + + if (targetWidth > screenWidth) { + float fixXOffset = dst.left; + if (fixXOffset > 0) { + dst.left -= fixXOffset; + dst.right -= fixXOffset; + } else if ((fixXOffset = screenWidth - dst.right) > 0) { + dst.left += fixXOffset; + dst.right += fixXOffset; + } + } else { + float left = (screenWidth - targetWidth) / 2; + dst.offsetTo(left, dst.top); + } + if (targetHeight > screenHeight) { + float fixYOffset = dst.top; + if (fixYOffset > 0) { + dst.top -= fixYOffset; + dst.bottom -= fixYOffset; + } else if ((fixYOffset = screenHeight - dst.bottom) > 0) { + dst.top += fixYOffset; + dst.bottom += fixYOffset; + } + } else { + float top = (screenHeight - targetHeight) / 2; + dst.offsetTo(dst.left, top); + } + } + + public void setScaleOffset(int scaleMode, int startPosition, float scaleValue) { + mScaleMode = scaleMode; + mStartPosition = startPosition; + mScaleValue = scaleValue; + + int screenWidth = getWidth(); + int screenHeight = getHeight(); + + if (mImageTexture == null || screenWidth == 0 || screenHeight == 0) { + mScaleOffsetDirty = true; + return; + } + + int textureWidth = mTextureWidth; + int textureHeight = mTextureHeight; + + // Set scale + float targetWidth; + float targetHeight; + switch (scaleMode) { + case SCALE_ORIGIN: + mScale = 1.0f; + targetWidth = textureWidth; + targetHeight = textureHeight; + break; + case SCALE_FIT_WIDTH: + mScale = (float) screenWidth / textureWidth; + targetWidth = screenWidth; + targetHeight = textureHeight * mScale; + break; + case SCALE_FIT_HEIGHT: + mScale = (float) screenHeight / textureHeight; + targetWidth = textureWidth * mScale; + targetHeight = screenHeight; + break; + case SCALE_FIT: + float scaleX = (float) screenWidth / textureWidth; + float scaleY = (float) screenHeight / textureHeight; + if (scaleX < scaleY) { + mScale = scaleX; + targetWidth = screenWidth; + targetHeight = textureHeight * scaleX; + } else { + mScale = scaleY; + targetWidth = textureWidth * scaleY; + targetHeight = screenHeight; + break; + } + break; + case SCALE_FIXED: + default: + mScale = scaleValue; + targetWidth = textureWidth * scaleValue; + targetHeight = textureHeight * scaleValue; + break; + } + + // adjust scale, not too big, not too small + if (mScale < SCALE_MIN) { + mScale = SCALE_MIN; + targetWidth = textureWidth * SCALE_MIN; + targetHeight = textureHeight * SCALE_MIN; + } else if (mScale > SCALE_MAX) { + mScale = SCALE_MAX; + targetWidth = textureWidth * SCALE_MAX; + targetHeight = textureHeight * SCALE_MAX; + } + + // Set mDst.left and mDst.right + RectF dst = mDst; + switch (startPosition) { + case START_POSITION_TOP_LEFT: + dst.left = 0; + dst.top = 0; + break; + case START_POSITION_TOP_RIGHT: + dst.left = screenWidth - targetWidth; + dst.top = 0; + break; + case START_POSITION_BOTTOM_LEFT: + dst.left = 0; + dst.top = screenHeight - targetHeight; + break; + case START_POSITION_BOTTOM_RIGHT: + dst.left = screenWidth - targetWidth; + dst.top = screenHeight - targetHeight; + break; + case START_POSITION_CENTER: + default: + dst.left = (screenWidth - targetWidth) / 2; + dst.top = (screenHeight - targetHeight) / 2; + break; + } + + // Set mDst.right and mDst.bottom + dst.right = dst.left + targetWidth; + dst.bottom = dst.top + targetHeight; + + // adjust position + adjustPosition(); + + mScaleOffsetDirty = false; + mPositionInRootDirty = true; + } + + public void scroll(int dx, int dy, int[] remain) { + // Only work after layout + if (mScaleOffsetDirty) { + setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + } + if (mScaleOffsetDirty) { + remain[0] = dx; + remain[1] = dy; + return; + } + + RectF dst = mDst; + int screenWidth = getWidth(); + int screenHeight = getHeight(); + float targetWidth = dst.width(); + float targetHeight = dst.height(); + + if (targetWidth > screenWidth) { + dst.left -= dx; + dst.right -= dx; + + float fixXOffset = dst.left; + if (fixXOffset > 0) { + dst.left -= fixXOffset; + dst.right -= fixXOffset; + remain[0] = -(int) fixXOffset; + } else if ((fixXOffset = screenWidth - dst.right) > 0) { + dst.left += fixXOffset; + dst.right += fixXOffset; + remain[0] = (int) fixXOffset; + } else { + remain[0] = 0; + } + } else { + remain[0] = dx; + } + if (targetHeight > screenHeight) { + dst.top -= dy; + dst.bottom -= dy; + + float fixYOffset = dst.top; + if (fixYOffset > 0) { + dst.top -= fixYOffset; + dst.bottom -= fixYOffset; + remain[1] = -(int) fixYOffset; + } else if ((fixYOffset = screenHeight - dst.bottom) > 0) { + dst.top += fixYOffset; + dst.bottom += fixYOffset; + remain[1] = (int) fixYOffset; + } else { + remain[1] = 0; + } + } else { + remain[1] = dy; + } + + if (dx != remain[0] || dy != remain[1]) { + mPositionInRootDirty = true; + invalidate(); + } + } + + public void scale(float focusX, float focusY, float scale) { + // Only work after layout + if (mScaleOffsetDirty) { + setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + } + if (mScaleOffsetDirty) { + return; + } + + if ((mScale == SCALE_MAX && scale >= 1.0f) || (mScale == SCALE_MIN && scale < 1.0f)) { + return; + } + + float newScale = mScale * scale; + newScale = MathUtils.clamp(newScale, SCALE_MIN, SCALE_MAX); + mScale = newScale; + RectF dst = mDst; + float left = (focusX - ((focusX - dst.left) * scale)); + float top = (focusY - ((focusY - dst.top) * scale)); + dst.set(left, top, + (left + (mImageTexture.getWidth() * newScale)), + (top + (mImageTexture.getHeight() * newScale))); + + // adjust position + adjustPosition(); + + mPositionInRootDirty = true; + invalidate(); + } + + private void applyPositionInRoot() { + int width = mImageTexture.getWidth(); + int height = mImageTexture.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(); + } + + mPositionInRootDirty = false; + } + + @Override + public void onRender(GLCanvas canvas) { + Texture texture = mImageTexture; + if (texture == null) { + return; + } + + if (mScaleOffsetDirty) { + setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + } + + if (mPositionInRootDirty) { + applyPositionInRoot(); + } + + if (!mSrcActual.isEmpty()) { + texture.draw(canvas, mSrcActual, mDstActual); + } + } + + @Override + public void invalidateImageTexture(ImageTexture who) { + invalidate(); + } +} diff --git a/app/src/main/java/com/hippo/glgallery/PagerLayoutManager.java b/app/src/main/java/com/hippo/glgallery/PagerLayoutManager.java new file mode 100644 index 000000000..b7905613c --- /dev/null +++ b/app/src/main/java/com/hippo/glgallery/PagerLayoutManager.java @@ -0,0 +1,1093 @@ +/* + * 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.glgallery; + +import android.content.Context; +import android.graphics.Rect; +import android.util.Log; +import android.view.animation.Interpolator; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; + +import com.hippo.glview.anim.Animation; +import com.hippo.glview.anim.FloatAnimation; +import com.hippo.glview.view.GLView; +import com.hippo.glview.widget.GLEdgeView; +import com.hippo.glview.widget.GLProgressView; +import com.hippo.glview.widget.GLTextureView; +import com.hippo.yorozuya.AnimationUtils; +import com.hippo.yorozuya.AssertUtils; +import com.hippo.yorozuya.MathUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +class PagerLayoutManager extends GalleryView.LayoutManager { + + private static final String TAG = PagerLayoutManager.class.getSimpleName(); + + @IntDef({MODE_LEFT_TO_RIGHT, MODE_RIGHT_TO_LEFT}) + @Retention(RetentionPolicy.SOURCE) + private @interface Mode {} + + public static final int MODE_LEFT_TO_RIGHT = 0; + public static final int MODE_RIGHT_TO_LEFT = 1; + + private static final Interpolator SMOOTH_SCROLLER_INTERPOLATOR = new Interpolator() { + @Override + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private GalleryView.Adapter mAdapter; + + private GLProgressView mProgress; + private String mErrorStr; + private GLTextureView mErrorView; + private GalleryPageView mPrevious; + private GalleryPageView mCurrent; + private GalleryPageView mNext; + + @Mode + private int mMode = MODE_RIGHT_TO_LEFT; + private int mScaleMode; + private int mStartPosition; + private float mScaleValue; + + private int mOffset; + private int mDeltaX; + private int mDeltaY; + private boolean mCanScrollBetweenPages = false; + private boolean mStopAnimationFinger; + + private int mInterval; + + private final int[] mScrollRemain = new int[2]; + private final float[] mScaleDefault = new float[4]; + + private final SmoothScroller mSmoothScroller; + private final PageFling mPageFling; + private final SmoothScaler mSmoothScaler; + private final OverScroller mOverScroller; + + // Current index + private int mIndex; + + private final Rect mTempRect = new Rect(); + + public PagerLayoutManager(Context context, @NonNull GalleryView galleryView, + int scaleMode, int startPoint, float scaleValue, int interval) { + super(galleryView); + + mScaleMode = scaleMode; + mStartPosition = startPoint; + mScaleValue = scaleValue; + + mInterval = interval; + mSmoothScroller = new SmoothScroller(); + mPageFling = new PageFling(context); + mSmoothScaler = new SmoothScaler(); + mOverScroller = new OverScroller(); + } + + public void setInterval(int interval) { + if (mInterval == interval) { + return; + } + + if (mAdapter != null) { + int index = getInternalCurrentIndex(); + GalleryView.Adapter adapter = onDetach(); + mInterval = interval; + onAttach(adapter); + setCurrentIndex(index); + mGalleryView.requestFill(); + } else { + mInterval = interval; + } + } + + private void resetParameters() { + mOffset = 0; + mDeltaX = 0; + mDeltaY = 0; + mCanScrollBetweenPages = false; + mStopAnimationFinger = false; + } + + private boolean cancelAllAnimations() { + boolean running = mSmoothScroller.isRunning() || + mPageFling.isRunning() || + mSmoothScaler.isRunning() || + mOverScroller.isRunning(); + mSmoothScroller.cancel(); + mPageFling.cancel(); + mSmoothScaler.cancel(); + mOverScroller.cancel(); + return running; + } + + public void setMode(@Mode int mode) { + if (mMode == mode) { + return; + } + + mMode = mode; + if (mAdapter != null) { + // It is attached, refill + // Cancel all animations + cancelAllAnimations(); + // Remove all view + removeProgress(); + removeErrorView(); + removeAllPages(); + // Reset parameters + resetParameters(); + // Request fill + mGalleryView.requestFill(); + } + } + + public void setScaleMode(int scaleMode) { + if (mScaleMode == scaleMode) { + return; + } + mScaleMode = scaleMode; + + if (mCurrent != null) { + mCurrent.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + } + if (mPrevious != null) { + mPrevious.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + } + if (mNext != null) { + mNext.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + } + } + + public void setStartPosition(int startPosition) { + if (mStartPosition == startPosition) { + return; + } + mStartPosition = startPosition; + + if (mCurrent != null) { + mCurrent.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + } + if (mPrevious != null) { + mPrevious.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + } + if (mNext != null) { + mNext.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + } + } + + @Override + public void onAttach(GalleryView.Adapter adapter) { + AssertUtils.assertNull("The PagerLayoutManager is attached", mAdapter); + AssertUtils.assertNotNull("The adapter is null", adapter); + mAdapter = adapter; + // Reset parameters + resetParameters(); + } + + private void removeProgress() { + if (mProgress != null) { + mGalleryView.removeComponent(mProgress); + mGalleryView.releaseProgress(mProgress); + mProgress = null; + } + } + + private void removeErrorView() { + if (mErrorView != null) { + mGalleryView.removeComponent(mErrorView); + mGalleryView.releaseErrorView(mErrorView); + mErrorView = null; + mErrorStr = null; + } + } + + private void removePage(@NonNull GalleryPageView page) { + mGalleryView.removeComponent(page); + mAdapter.unbind(page); + mGalleryView.releasePage(page); + } + + private void removeAllPages() { + // Remove gallery view + if (mPrevious != null) { + removePage(mPrevious); + mPrevious = null; + } + if (mCurrent != null) { + removePage(mCurrent); + mCurrent = null; + } + if (mNext != null) { + removePage(mNext); + mNext = null; + } + } + + @Override + public GalleryView.Adapter onDetach() { + AssertUtils.assertNotNull("The PagerLayoutManager is not attached", mAdapter); + + // Cancel all animations + cancelAllAnimations(); + + // Remove all view + removeProgress(); + removeErrorView(); + removeAllPages(); + + // Clear iterator + GalleryView.Adapter adapter = mAdapter; + mAdapter = null; + + return adapter; + } + + private GalleryPageView getLeftPage() { + switch (mMode) { + case MODE_LEFT_TO_RIGHT: + return mPrevious; + default: + case MODE_RIGHT_TO_LEFT: + return mNext; + } + } + + private GalleryPageView getRightPage() { + switch (mMode) { + case MODE_LEFT_TO_RIGHT: + return mNext; + default: + case MODE_RIGHT_TO_LEFT: + return mPrevious; + } + } + + private GalleryPageView obtainPage() { + GalleryPageView page = mGalleryView.obtainPage(); + page.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + return page; + } + + private void layoutPage(GalleryPageView page, int widthSpec, int heightSpec, + int left, int top, int right, int bottom) { + Rect rect = mTempRect; + page.getValidRect(rect); + boolean oldValid = !rect.isEmpty(); + page.measure(widthSpec, heightSpec); + page.layout(left, top, right, bottom); + page.getValidRect(rect); + boolean newValid = !rect.isEmpty(); + if (!oldValid && newValid) { + page.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); + } + } + + @Override + public void onFill() { + GalleryView.Adapter adapter = mAdapter; + GalleryView galleryView = mGalleryView; + AssertUtils.assertNotNull("The PagerLayoutManager is not attached", adapter); + + int width = galleryView.getWidth(); + int height = galleryView.getHeight(); + int size = adapter.size(); + String errorStr = adapter.getError(); + + if (size == GalleryProvider.STATE_WAIT) { // Wait here, show progress bar + // Remove error view and all pages + removeErrorView(); + removeAllPages(); + + // Ensure progress + if (mProgress == null) { + mProgress = galleryView.obtainProgress(); + galleryView.addComponent(mProgress); + } + + // Place progress center + placeCenter(mProgress); + } else if (size <= GalleryProvider.STATE_ERROR || size == 0) { // Get error or empty, show error text + // Ensure error is not null + if (0 == size) { + errorStr = galleryView.getEmptyStr(); + } else if (null == errorStr) { + errorStr = galleryView.getDefaultErrorStr(); + } + + // Remove progress and all pages + removeProgress(); + removeAllPages(); + + // Ensure error view + if (mErrorView == null) { + mErrorView = galleryView.obtainErrorView(); + galleryView.addComponent(mErrorView); + } + + // Update error string + if (!errorStr.equals(mErrorStr)) { + mErrorStr = errorStr; + galleryView.bindErrorView(mErrorView, errorStr); + } + + // Place error view center + placeCenter(mErrorView); + } else { + // Remove progress and error view + removeProgress(); + removeErrorView(); + + // Ensure index in range + int index = mIndex; + if (index < 0) { + index = 0; + mIndex = index; + removeAllPages(); + Log.e(TAG, "index < 0, index = " + index); + } else if (index >= size) { + index = size - 1; + mIndex = index; + removeAllPages(); + Log.e(TAG, "index >= size, index = " + index + ", size = " + size); + } + + // Ensure pages + if (mCurrent == null) { + mCurrent = obtainPage(); + galleryView.addComponent(mCurrent); + adapter.bind(mCurrent, index); + } + if (mPrevious == null && index > 0) { + mPrevious = obtainPage(); + galleryView.addComponent(mPrevious); + adapter.bind(mPrevious, index - 1); + } else if (mPrevious != null && index == 0) { + removePage(mPrevious); + } + if (mNext == null && index < size - 1) { + mNext = obtainPage(); + galleryView.addComponent(mNext); + adapter.bind(mNext, index + 1); + } else if (mNext != null && index == size - 1) { + removePage(mNext); + } + + GalleryPageView leftPage = getLeftPage(); + GalleryPageView rightPage = getRightPage(); + + // Fix offset + final int min = rightPage == null ? 0 : -width - mInterval + 1; + final int max = leftPage == null ? 0 : width + mInterval - 1; + mOffset = MathUtils.clamp(mOffset, min, max); + + // Measure and layout pages + final int offset = mOffset; + final int widthSpec = GLView.MeasureSpec.makeMeasureSpec(width, GLView.MeasureSpec.EXACTLY); + final int heightSpec = GLView.MeasureSpec.makeMeasureSpec(height, GLView.MeasureSpec.EXACTLY); + if (mCurrent != null) { + layoutPage(mCurrent, widthSpec, heightSpec, + offset, 0, width + offset, height); + } + if (leftPage != null) { + layoutPage(leftPage, widthSpec, heightSpec, + -mInterval - width + offset, 0, -mInterval + offset, height); + } + if (rightPage != null) { + layoutPage(rightPage, widthSpec, heightSpec, + width + mInterval + offset, 0, width + mInterval + width + offset, height); + } + } + } + + @Override + public void onDown() { + mDeltaX = 0; + mDeltaY = 0; + mStopAnimationFinger = cancelAllAnimations(); + } + + @Override + public void onUp() { + mGalleryView.getEdgeView().onRelease(); + + if (mCurrent == null) { + return; + } + + // Scroll + if (mOffset != 0) { + int width = mGalleryView.getWidth(); + int dx; + if (mOffset >= mInterval && getLeftPage() != null) { + dx = mOffset - width - mInterval; + } else if (mOffset <= -mInterval && getRightPage() != null) { + dx = mOffset + width + mInterval; + } else { + dx = mOffset; + } + final float pageDelta = 7 * (float) Math.abs(mOffset) / (width + mInterval); + int duration = (int) ((pageDelta + 1) * 100); + mSmoothScroller.startSmoothScroll(dx, 0, duration); + } + } + + @Override + public void onDoubleTapConfirmed(float x, float y) { + if (mCurrent == null || !mCurrent.getImageView().isLoaded()) { + return; + } + + float[] scales = mScaleDefault; + ImageView image = mCurrent.getImageView(); + image.getScaleDefault(scales); + float scale = image.getScale(); + float endScale = scales[0]; + for (float value: scales) { + if (scale < value - 0.01f) { + endScale = value; + break; + } + } + + mSmoothScaler.startSmoothScaler(x, y, scale, endScale, 300); + } + + @Override + public void onLongPress(float x, float y) {} + + private void pagePrevious() { + if (mIndex <= 0) { + return; + } + mIndex--; + + if (mNext != null) { + removePage(mNext); + } + mNext = mCurrent; + mCurrent = mPrevious; + mPrevious = null; + + if (mIndex > 0) { + mPrevious = obtainPage(); + mGalleryView.addComponent(mPrevious); + mAdapter.bind(mPrevious, mIndex - 1); + } + } + + private void pageNext() { + GalleryView.Adapter adapter = mAdapter; + int size = adapter.size(); + if (mIndex >= size - 1) { + return; + } + mIndex++; + + if (mPrevious != null) { + removePage(mPrevious); + } + mPrevious = mCurrent; + mCurrent = mNext; + mNext = null; + + if (mIndex < size - 1) { + mNext = obtainPage(); + mGalleryView.addComponent(mNext); + adapter.bind(mNext, mIndex + 1); + } + } + + private void pageLeft() { + switch (mMode) { + case MODE_LEFT_TO_RIGHT: + pagePrevious(); + break; + default: + case MODE_RIGHT_TO_LEFT: + pageNext(); + break; + } + } + + private void pageRight() { + switch (mMode) { + case MODE_LEFT_TO_RIGHT: + pageNext(); + break; + default: + case MODE_RIGHT_TO_LEFT: + pagePrevious(); + break; + } + } + + private int scrollBetweenPages(int dx) { + GalleryPageView leftPage = getLeftPage(); + GalleryPageView rightPage = getRightPage(); + int width = mGalleryView.getWidth(); + + int remain; + if (dx < 0) { // Try to show left + int limit; + if (leftPage == null) { + limit = 0; + } else { + limit = width + mInterval; + } + + if (dx > mOffset - limit) { + remain = 0; + mOffset -= dx; + } else { + // Go to left page if left page not null + if (leftPage != null) { + pageLeft(); + } + remain = dx + limit - mOffset; + mOffset = 0; + } + } else { // Try to show right + int limit; + if (rightPage == null) { + limit = 0; + } else { + limit = - width - mInterval; + } + + if (dx < mOffset - limit) { + remain = 0; + mOffset -= dx; + } else { + // Go to right page if right page not null + if (rightPage != null) { + pageRight(); + } + remain = dx + limit - mOffset; + mOffset = 0; + } + } + + return remain; + } + + public void overScrollEdge(int dx, int dy, float x, float y) { + GLEdgeView edgeView = mGalleryView.getEdgeView(); + + mDeltaX += dx; + mDeltaY += dy; + + if (mDeltaX < 0) { + edgeView.onPull(-mDeltaX, y, GLEdgeView.LEFT); + if (!edgeView.isFinished(GLEdgeView.RIGHT)) { + edgeView.onRelease(GLEdgeView.RIGHT); + } + } else if (mDeltaX > 0) { + edgeView.onPull(mDeltaX, y, GLEdgeView.RIGHT); + if (!edgeView.isFinished(GLEdgeView.LEFT)) { + edgeView.onRelease(GLEdgeView.LEFT); + } + } + + if (mCurrent.getImageView().canFlingVertically()) { + if (mDeltaY < 0) { + edgeView.onPull(-mDeltaY, x, GLEdgeView.TOP); + if (!edgeView.isFinished(GLEdgeView.BOTTOM)) { + edgeView.onRelease(GLEdgeView.BOTTOM); + } + } else if (mDeltaY > 0) { + edgeView.onPull(mDeltaY, x, GLEdgeView.BOTTOM); + if (!edgeView.isFinished(GLEdgeView.TOP)) { + edgeView.onRelease(GLEdgeView.TOP); + } + } + } + } + + public void scrollInternal(float dx, float dy, float x, float y) { + if (mCurrent == null) { + return; + } + + boolean needFill = false; + boolean canImageScroll = true; + int remainX = (int) dx; + int remainY = (int) dy; + + if (mGalleryView.isFirstScroll()) { + mCanScrollBetweenPages = Math.abs(dx) > Math.abs(dy) * 1.5; + } + + while (remainX != 0 || remainY != 0) { + if (mOffset == 0 && canImageScroll) { + ImageView image = mCurrent.getImageView(); + image.scroll(remainX, remainY, mScrollRemain); + remainX = mScrollRemain[0]; + remainY = mScrollRemain[1]; + canImageScroll = false; + mDeltaX = 0; + mDeltaY = 0; + } else if (remainX == 0 || + (getLeftPage() == null && mOffset == 0 && remainX < 0) || + (getRightPage() == null && mOffset == 0 && remainX > 0)) { + // On edge + overScrollEdge(remainX, remainY, x, y); + remainX = 0; + remainY = 0; + } else if (mCanScrollBetweenPages) { + remainX = scrollBetweenPages(remainX); + canImageScroll = true; + needFill = true; + mDeltaX = 0; + mDeltaY = 0; + } else { + remainX = 0; + remainY = 0; + mDeltaX = 0; + mDeltaY = 0; + } + } + + if (needFill) { + mGalleryView.requestFill(); + } + } + + @Override + public void onScroll(float dx, float dy, float totalX, float totalY, float x, float y) { + scrollInternal(dx, dy, x, y); + } + + @Override + public void onFling(float velocityX, float velocityY) { + if (mCurrent == null || mOffset != 0 || !mCurrent.getImageView().isLoaded() || + !mCurrent.getImageView().canFling()) { + return; + } + + ImageView image = mCurrent.getImageView(); + mPageFling.startFling((int) velocityX, image.getMinDx(), image.getMaxDx(), + (int) velocityY, image.getMinDy(), image.getMaxDy()); + } + + @Override + public boolean canScale() { + return mCurrent != null && mOffset == 0 && mCurrent.getImageView().isLoaded(); + } + + @Override + public void onScale(float focusX, float focusY, float scale) { + if (mCurrent == null || !mCurrent.getImageView().isLoaded()) { + return; + } + + mCurrent.getImageView().scale(focusX, focusY, scale); + mScaleValue = mCurrent.getImageView().getScale(); + } + + @Override + public boolean onUpdateAnimation(long time) { + boolean invalidate = mSmoothScroller.calculate(time); + invalidate |= mPageFling.calculate(time); + invalidate |= mSmoothScaler.calculate(time); + invalidate |= mOverScroller.calculate(time); + return invalidate; + } + + @Override + public void onDataChanged() { + AssertUtils.assertNotNull("The PagerLayoutManager is not attached", mAdapter); + + // Cancel all animations + cancelAllAnimations(); + // Remove all views + removeProgress(); + removeErrorView(); + removeAllPages(); + // Reset parameters + resetParameters(); + mGalleryView.requestFill(); + } + + @Override + public void onPageLeft() { + int size = mAdapter.size(); + if (size <= 0 || mCurrent == null) { + return; + } + + if (mMode == MODE_LEFT_TO_RIGHT) { + if (mIndex == 0) { + mOverScroller.overScroll(GLEdgeView.LEFT); + } else { + setCurrentIndex(mIndex - 1); + } + } else { + if (mIndex >= size - 1) { + mOverScroller.overScroll(GLEdgeView.LEFT); + } else { + setCurrentIndex(mIndex + 1); + } + } + } + + @Override + public void onPageRight() { + int size = mAdapter.size(); + if (size <= 0 || mCurrent == null) { + return; + } + + if (mMode == MODE_LEFT_TO_RIGHT) { + if (mIndex >= size - 1) { + mOverScroller.overScroll(GLEdgeView.RIGHT); + } else { + setCurrentIndex(mIndex + 1); + } + } else { + if (mIndex == 0) { + mOverScroller.overScroll(GLEdgeView.RIGHT); + } else { + setCurrentIndex(mIndex - 1); + } + } + } + + @Override + public boolean isTapOrPressEnable() { + return !mStopAnimationFinger; + } + + @Override + public GalleryPageView findPageByIndex(int index) { + if (mCurrent != null && mCurrent.getIndex() == index) { + return mCurrent; + } + if (mPrevious != null && mPrevious.getIndex() == index) { + return mPrevious; + } + if (mNext != null && mNext.getIndex() == index) { + return mNext; + } + return null; + } + + @Override + public int getCurrentIndex() { + if (mCurrent != null) { + return mCurrent.getIndex(); + } else { + return GalleryPageView.INVALID_INDEX; + } + } + + @Override + public void setCurrentIndex(int index) { + int size = mAdapter.size(); + if (size <= 0) { + // Can't get size now, assume size is MAX + size = Integer.MAX_VALUE; + } + if (index == mIndex || index < 0 || index >= size) { + return; + } + if (mCurrent == null) { + mIndex = index; + } else if (index == mIndex - 1) { + // Cancel all animations + cancelAllAnimations(); + // Reset parameters + resetParameters(); + // Go to previous + pagePrevious(); + // Request fill + mGalleryView.requestFill(); + } else if (index == mIndex + 1) { + // Cancel all animations + cancelAllAnimations(); + // Reset parameters + resetParameters(); + // Go to next + pageNext(); + // Request fill + mGalleryView.requestFill(); + } else { + mIndex = index; + // It is attached, refill + // Cancel all animations + cancelAllAnimations(); + // Remove all view + removeProgress(); + removeErrorView(); + removeAllPages(); + // Reset parameters + resetParameters(); + // Request fill + mGalleryView.requestFill(); + } + } + + @Override + public int getIndexUnder(float x, float y) { + if (mCurrent == null) { + return GalleryPageView.INVALID_INDEX; + } else { + int intX = (int) x; + int intY = (int) y; + if (mCurrent.bounds().contains(intX, intY)) { + return mCurrent.getIndex(); + } else if (mPrevious != null && mPrevious.bounds().contains(intX, intY)) { + return mPrevious.getIndex(); + } else if (mNext != null && mNext.bounds().contains(intX, intY)) { + return mNext.getIndex(); + } else { + return GalleryPageView.INVALID_INDEX; + } + } + } + + @Override + int getInternalCurrentIndex() { + int currentIndex = getCurrentIndex(); + if (currentIndex == GalleryPageView.INVALID_INDEX) { + currentIndex = mIndex; + } + return currentIndex; + } + + private class SmoothScroller extends Animation { + + private int mDx; + private int mDy; + private int mLastX; + private int mLastY; + + public SmoothScroller() { + setInterpolator(SMOOTH_SCROLLER_INTERPOLATOR); + } + + public void startSmoothScroll(int dx, int dy, int duration) { + mDx = dx; + mDy = dy; + mLastX = 0; + mLastY = 0; + setDuration(duration); + start(); + mGalleryView.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 = scrollBetweenPages(offsetX); + // Avoid loop infinitely + if (offsetX == oldOffsetX) { + break; + } else { + mGalleryView.requestFill(); + } + } + mLastX = x; + mLastY = y; + } + } + + private class PageFling extends Fling { + + private int mVelocityX; + private int mVelocityY; + private int mDx; + private int mDy; + private int mLastX; + private int mLastY; + private final int[] mTemp = new int[2]; + + public PageFling(Context context) { + super(context); + } + + public void startFling(int velocityX, int minX, int maxX, + int velocityY, int minY, int maxY) { + mVelocityX = velocityX; + mVelocityY = velocityY; + 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; + } + + if (mDx == 0 && mDy == 0) { + return; + } + + setDuration(Math.max(durationX, durationY)); + start(); + mGalleryView.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 (mCurrent != null && (offsetX != 0 || offsetY != 0)) { + mCurrent.getImageView().scroll(-offsetX, -offsetY, mTemp); + } + mLastX = x; + mLastY = y; + } + + @Override + protected void onFinish() { + if (mCurrent == null) { + return; + } + + GLEdgeView edgeView = mGalleryView.getEdgeView(); + ImageView imageView = mCurrent.getImageView(); + if (imageView.canFlingHorizontally()) { + if (mVelocityX > 0 && getLeftPage() == null && imageView.getMaxDx() == 0 && + edgeView.isFinished(GLEdgeView.LEFT)) { + edgeView.onAbsorb(mVelocityX, GLEdgeView.LEFT); + } else if (mVelocityX < 0 && getRightPage() == null && imageView.getMinDx() == 0 && + edgeView.isFinished(GLEdgeView.RIGHT)) { + edgeView.onAbsorb(-mVelocityX, GLEdgeView.RIGHT); + } + } + if (imageView.canFlingVertically()) { + if (mVelocityY > 0 && imageView.getMaxDy() == 0 && + edgeView.isFinished(GLEdgeView.TOP)) { + edgeView.onAbsorb(mVelocityY, GLEdgeView.TOP); + } else if (mVelocityY < 0 && imageView.getMinDy() == 0 && + edgeView.isFinished(GLEdgeView.BOTTOM)) { + edgeView.onAbsorb(-mVelocityY, GLEdgeView.BOTTOM); + } + } + } + } + + private class SmoothScaler extends Animation { + + private float mFocusX; + private float mFocusY; + private float mStartScale; + private float mEndScale; + private float mLastScale; + + public SmoothScaler() { + setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR); + } + + public void startSmoothScaler(float focusX, float focusY, + float startScale, float endScale, int duration) { + mFocusX = focusX; + mFocusY = focusY; + mStartScale = startScale; + mEndScale = endScale; + mLastScale = startScale; + setDuration(duration); + start(); + mGalleryView.invalidate(); + } + + @Override + protected void onCalculate(float progress) { + if (mCurrent == null) { + return; + } + + float scale = MathUtils.lerp(mStartScale, mEndScale, progress); + mCurrent.getImageView().scale(mFocusX, mFocusY, scale / mLastScale); + mLastScale = scale; + mScaleValue = scale; + } + } + + private class OverScroller extends FloatAnimation { + + private int mDirection; + private int mPosition; + + public OverScroller() { + setDuration(300L); + } + + public void overScroll(int direction) { + mDirection = direction; + int range; + switch (mDirection) { + case GLEdgeView.LEFT: + case GLEdgeView.RIGHT: + range = mGalleryView.getWidth() / 7; + mPosition = mGalleryView.getHeight() / 2; + break; + case GLEdgeView.TOP: + case GLEdgeView.BOTTOM: + range = mGalleryView.getHeight() / 7; + mPosition = mGalleryView.getWidth() / 2; + break; + default: + return; + } + setRange(0, range); + start(); + mGalleryView.invalidate(); + } + + @Override + protected void onCalculate(float progress) { + super.onCalculate(progress); + mGalleryView.getEdgeView().onPull(get(), mPosition, mDirection); + } + + @Override + protected void onFinish() { + mGalleryView.getEdgeView().onRelease(mDirection); + } + } +} diff --git a/app/src/main/java/com/hippo/glgallery/ScrollLayoutManager.java b/app/src/main/java/com/hippo/glgallery/ScrollLayoutManager.java new file mode 100644 index 000000000..0e9eac717 --- /dev/null +++ b/app/src/main/java/com/hippo/glgallery/ScrollLayoutManager.java @@ -0,0 +1,1241 @@ +/* + * 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.glgallery; + +import android.content.Context; +import android.graphics.Rect; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.hippo.glview.anim.Animation; +import com.hippo.glview.anim.FloatAnimation; +import com.hippo.glview.view.GLView; +import com.hippo.glview.widget.GLEdgeView; +import com.hippo.glview.widget.GLProgressView; +import com.hippo.glview.widget.GLTextureView; +import com.hippo.yorozuya.AnimationUtils; +import com.hippo.yorozuya.AssertUtils; +import com.hippo.yorozuya.MathUtils; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +class ScrollLayoutManager extends GalleryView.LayoutManager { + + private static final String TAG = ScrollLayoutManager.class.getSimpleName(); + + private static final float RESERVATION = 1f; + + private static final float MAX_SCALE = 2.0f; + private static final float MIN_SCALE = 1.0f; + private static final float SCALE_ERROR = 0.01f; + + private static final int INVALID_TOP = Integer.MAX_VALUE; + + private GalleryView.Adapter mAdapter; + + private GLProgressView mProgress; + private String mErrorStr; + private GLTextureView mErrorView; + private final LinkedList mPages = new LinkedList<>(); + private final LinkedList mTempPages = new LinkedList<>(); + + private float mScale = 0.5f; + private int mOffsetX; + private int mOffsetY; + private int mDeltaX; + private int mDeltaY; + private int mKeepTopPageIndex = GalleryPageView.INVALID_INDEX; + private int mKeepTop = INVALID_TOP; + private int mFirstShownPageIndex = GalleryPageView.INVALID_INDEX; + private boolean mScrollUp; + private boolean mFlingUp; + private boolean mStopAnimationFinger; + + private int mInterval; + + private final PageFling mPageFling; + private final SmoothScaler mSmoothScaler; + private final OverScroller mOverScroller; + + // Current index + private int mIndex; + + private int mBottomStateBottom; + private boolean mBottomStateHasNext; + + public ScrollLayoutManager(Context context, @NonNull GalleryView galleryView, int interval) { + super(galleryView); + + mInterval = interval; + mPageFling = new PageFling(context); + mSmoothScaler = new SmoothScaler(); + mOverScroller = new OverScroller(); + } + + public void setInterval(int interval) { + if (mInterval == interval) { + return; + } + + if (mAdapter != null) { + int index = getInternalCurrentIndex(); + GalleryView.Adapter adapter = onDetach(); + mInterval = interval; + onAttach(adapter); + setCurrentIndex(index); + mGalleryView.requestFill(); + } else { + mInterval = interval; + } + } + + private void resetParameters() { + mScale = 1.0f; + mOffsetX = 0; + mOffsetY = 0; + mDeltaX = 0; + mDeltaY = 0; + mKeepTopPageIndex = GalleryPageView.INVALID_INDEX; + mKeepTop = INVALID_TOP; + mFirstShownPageIndex = GalleryPageView.INVALID_INDEX; + mScrollUp = false; + mFlingUp = false; + mStopAnimationFinger = false; + } + + // Return true for animations are running + private boolean cancelAllAnimations() { + boolean running = mPageFling.isRunning() || + mSmoothScaler.isRunning() || + mOverScroller.isRunning(); + mPageFling.cancel(); + mSmoothScaler.cancel(); + mOverScroller.cancel(); + return running; + } + + @Override + public void onAttach(GalleryView.Adapter adapter) { + AssertUtils.assertNull("The ScrollLayoutManager is attached", mAdapter); + AssertUtils.assertNotNull("The iterator is null", adapter); + mAdapter = adapter; + // Reset parameters + resetParameters(); + } + + private void removeProgress() { + if (mProgress != null) { + mGalleryView.removeComponent(mProgress); + mGalleryView.releaseProgress(mProgress); + mProgress = null; + } + } + + private void removeErrorView() { + if (mErrorView != null) { + mGalleryView.removeComponent(mErrorView); + mGalleryView.releaseErrorView(mErrorView); + mErrorView = null; + mErrorStr = null; + } + } + + private void removePage(@NonNull GalleryPageView page) { + mGalleryView.removeComponent(page); + mAdapter.unbind(page); + mGalleryView.releasePage(page); + } + + private void removeAllPages() { + for (GalleryPageView page : mPages) { + removePage(page); + } + mPages.clear(); + } + + @Override + public GalleryView.Adapter onDetach() { + AssertUtils.assertNotNull("The PagerLayoutManager is not attached", mAdapter); + + // Cancel all animations + cancelAllAnimations(); + + // Remove all view + removeProgress(); + removeErrorView(); + removeAllPages(); + + // Clear iterator + GalleryView.Adapter iterator = mAdapter; + mAdapter = null; + + return iterator; + } + + private GalleryPageView obtainPage() { + GalleryPageView page = mGalleryView.obtainPage(); + page.getImageView().setScaleOffset(ImageView.SCALE_FIT, ImageView.START_POSITION_TOP_RIGHT, 1.0f); + return page; + } + + private GalleryPageView getPageForIndex(List pages, int index, boolean remove) { + for (Iterator iterator = pages.iterator(); iterator.hasNext();) { + GalleryPageView page = iterator.next(); + if (index == page.getIndex()) { + if (remove) { + iterator.remove(); + } + return page; + } + } + return null; + } + + private boolean isInScreen(GalleryPageView page) { + int height = mGalleryView.getHeight(); + Rect bound = page.bounds(); + int pageTop = bound.top; + int pageBottom = bound.bottom; + return (pageTop >= 0 && pageTop < height) || (pageBottom > 0 && pageBottom <= height) || + (pageTop < 0 && pageBottom > height); + } + + private float getReservation() { + return Math.max(RESERVATION, (((1 + 2 * RESERVATION) * mScale) - 1) / 2); + } + + private void fillPages(int startIndex, int startOffset) { + final GalleryView.Adapter adapter = mAdapter; + final GalleryView galleryView = mGalleryView; + final LinkedList pages = mPages; + final LinkedList tempPages = mTempPages; + final int width = galleryView.getWidth(); + final int height = galleryView.getHeight(); + final int pageWidth = (int) (width * mScale); + final int size = adapter.size(); + final int interval = mInterval; + final float reservation = getReservation(); + final int minY = (int) (-height * reservation); + final int maxY = (int) (height * (1 + reservation)); + final int widthSpec = GLView.MeasureSpec.makeMeasureSpec(pageWidth, GLView.MeasureSpec.EXACTLY); + final int heightSpec = GLView.MeasureSpec.makeMeasureSpec(height, GLView.MeasureSpec.UNSPECIFIED); + + // Fix start index and start offset + if (startIndex < 0) { + startIndex = 0; + startOffset = 0; + } else if (startIndex >= size) { + startIndex = size - 1; + startOffset = 0; + } else if (startOffset < minY) { + while (true) { + GalleryPageView page = getPageForIndex(pages, startIndex, false); + if (null == page) { + startOffset = minY; + break; + } else { + page.measure(widthSpec, heightSpec); + if (startOffset + page.getHeight() > minY) { + break; + } else if (size - 1 == startIndex) { + startOffset = 0; + break; + } else { + ++startIndex; + startOffset += page.getHeight() + interval; + if (startOffset >= minY) { + break; + } + } + } + } + } else if (startOffset >= maxY) { + if (0 == startIndex) { + startOffset = 0; + } else { + --startIndex; + int startBottomOffset = startOffset - interval; + while (true) { + GalleryPageView page = getPageForIndex(pages, startIndex, false); + if (null == page) { + startOffset = maxY - 1; + break; + } else { + page.measure(widthSpec, heightSpec); + startOffset = startBottomOffset - page.getHeight(); + if (startOffset < maxY) { + break; + } else if (0 == startIndex) { + startOffset = 0; + break; + } else { + --startIndex; + startBottomOffset = startOffset - interval; + } + } + } + } + } + + // Put page to temp list + tempPages.addAll(pages); + pages.clear(); + + // Sanitize offsetX + int margin = pageWidth - width; + if (margin >= 0) { + mOffsetX = MathUtils.clamp(mOffsetX, -margin, 0); + } else { + mOffsetX = -margin / 2; + } + + // Layout start page + GalleryPageView page = getPageForIndex(tempPages, startIndex, true); + if (null == page) { + page = obtainPage(); + galleryView.addComponent(page); + adapter.bind(page, startIndex); + } + pages.add(page); + page.measure(widthSpec, heightSpec); + page.layout(mOffsetX, startOffset, mOffsetX + pageWidth, startOffset + page.getMeasuredHeight()); + + // Prepare for check up and down + int bottomOffset = startOffset - interval; + int topOffset = startOffset + page.getMeasuredHeight() + interval; + + // Check up + int index = startIndex - 1; + while (bottomOffset > minY && index >= 0) { + page = getPageForIndex(tempPages, index, true); + if (null == page) { + page = obtainPage(); + galleryView.addComponent(page); + adapter.bind(page, index); + } + pages.addFirst(page); + page.measure(widthSpec, heightSpec); + page.layout(mOffsetX, bottomOffset - page.getMeasuredHeight(), mOffsetX + pageWidth, bottomOffset); + // Update + bottomOffset -= page.getMeasuredHeight() + interval; + index--; + } + + // Avoid space in top + page = pages.getFirst(); + if (0 == page.getIndex() && page.bounds().top > 0) { + int offset = -page.bounds().top; + for (GalleryPageView p: pages) { + p.offsetTopAndBottom(offset); + } + topOffset += offset; + } + + // Check down + index = startIndex + 1; + while (topOffset < maxY && index < size) { + page = getPageForIndex(tempPages, index, true); + if (null == page) { + page = obtainPage(); + galleryView.addComponent(page); + adapter.bind(page, index); + } + pages.addLast(page); + page.measure(widthSpec, heightSpec); + page.layout(mOffsetX, topOffset, mOffsetX + pageWidth, topOffset + page.getMeasuredHeight()); + // Update + topOffset += page.getMeasuredHeight() + interval; + index++; + } + + // Avoid space in bottom + if (size - 1 == pages.getLast().getIndex()) { + while (true) { + page = pages.getLast(); + int pagesBottom = page.bounds().bottom; + if (pagesBottom >= height) { + break; + } + page = pages.getFirst(); + index = page.getIndex(); + if (0 == index) { + break; + } + --index; + int pagesTop = page.bounds().top; + + page = getPageForIndex(tempPages, index, true); + if (null == page) { + page = obtainPage(); + galleryView.addComponent(page); + adapter.bind(page, index); + } + pages.addFirst(page); + page.measure(widthSpec, heightSpec); + + int offset = Math.min(height - pagesBottom, page.getMeasuredHeight()); + for (GalleryPageView p: pages) { + p.offsetTopAndBottom(offset); + } + int bottom = pagesTop - interval + offset; + page.layout(mOffsetX, bottom - page.getMeasuredHeight(), mOffsetX + pageWidth, bottom); + } + } + + // Remove remain page + for (GalleryPageView p : tempPages) { + removePage(p); + } + tempPages.clear(); + + // Update state + if (!pages.isEmpty()) { + page = pages.getFirst(); + mIndex = page.getIndex(); + mOffsetY = page.bounds().top; + } + } + + @Override + public void onFill() { + GalleryView.Adapter adapter = mAdapter; + GalleryView galleryView = mGalleryView; + AssertUtils.assertNotNull("The PagerLayoutManager is not attached", adapter); + + int size = adapter.size(); + String errorStr = adapter.getError(); + + if (size == GalleryProvider.STATE_WAIT) { // Wait here, show progress bar + // Remove error view and all pages + removeErrorView(); + removeAllPages(); + + // Ensure progress + if (mProgress == null) { + mProgress = galleryView.obtainProgress(); + galleryView.addComponent(mProgress); + } + + // Place progress center + placeCenter(mProgress); + } else if (size <= GalleryProvider.STATE_ERROR || size == 0) { // Get error or empty, show error text + // Ensure error is not null + if (0 == size) { + errorStr = galleryView.getEmptyStr(); + } else if (null == errorStr) { + errorStr = galleryView.getDefaultErrorStr(); + } + + // Remove progress and all pages + removeProgress(); + removeAllPages(); + + // Ensure error view + if (mErrorView == null) { + mErrorView = galleryView.obtainErrorView(); + galleryView.addComponent(mErrorView); + } + + // Update error string + if (!errorStr.equals(mErrorStr)) { + mErrorStr = errorStr; + galleryView.bindErrorView(mErrorView, errorStr); + } + + // Place error view center + placeCenter(mErrorView); + } else { + LinkedList pages = mPages; + + // Remove progress and error view + removeProgress(); + removeErrorView(); + + // Ensure index in range + int index = mIndex; + if (index < 0) { + Log.e(TAG, "index < 0, index = " + index); + index = 0; + mIndex = index; + removeAllPages(); + } else if (index >= size) { + Log.e(TAG, "index >= size, index = " + index + ", size = " + size); + index = size - 1; + mIndex = index; + removeAllPages(); + } + + // Find keep index and keep top + int keepTop = INVALID_TOP; + int keepTopIndex; + if (GalleryPageView.INVALID_INDEX != mKeepTopPageIndex) { + keepTopIndex = mKeepTopPageIndex; + keepTop = mKeepTop; + } else if (GalleryPageView.INVALID_INDEX != mFirstShownPageIndex) { + keepTopIndex = mFirstShownPageIndex; + } else { + keepTopIndex = GalleryPageView.INVALID_INDEX; + } + if (GalleryPageView.INVALID_INDEX != keepTopIndex && INVALID_TOP == keepTop) { + keepTop = mOffsetY; + for (GalleryPageView page : pages) { + // Check keep page + if (keepTopIndex == page.getIndex()) { + break; + } + keepTop += page.getHeight() + mInterval; + } + } + + int startIndex; + int startOffset; + if (GalleryPageView.INVALID_INDEX != keepTopIndex) { + startIndex = keepTopIndex; + startOffset = keepTop; + } else { + startIndex = mIndex; + startOffset = mOffsetY; + } + fillPages(startIndex, startOffset); + + // Get first shown image + mFirstShownPageIndex = GalleryPageView.INVALID_INDEX; + for (GalleryPageView page : mPages) { + // Check first shown loaded page + if ((mScrollUp || mFlingUp) && !page.isLoaded()) { + continue; + } + + if (isInScreen(page)) { + mFirstShownPageIndex = page.getIndex(); + break; + } + } + } + } + + @Override + public void onDown() { + mDeltaX = 0; + mDeltaY = 0; + mScrollUp = false; + mStopAnimationFinger = cancelAllAnimations(); + } + + @Override + public void onUp() { + mScrollUp = false; + mGalleryView.getEdgeView().onRelease(); + } + + @Override + public void onDoubleTapConfirmed(float x, float y) { + if (mPages.size() <= 0) { + return; + } + + float startScale = mScale; + float endScale; + if (startScale < MAX_SCALE - SCALE_ERROR) { + endScale = MAX_SCALE; + } else { + endScale = MIN_SCALE; + } + + mSmoothScaler.startSmoothScaler(x, y, startScale, endScale, 300); + } + + @Override + public void onLongPress(float x, float y) { + + } + + public void overScrollEdge(int dx, int dy, float x, float y) { + GLEdgeView edgeView = mGalleryView.getEdgeView(); + + mDeltaX += dx; + mDeltaY += dy; + + if (mDeltaX < 0) { + edgeView.onPull(-mDeltaX, y, GLEdgeView.LEFT); + if (!edgeView.isFinished(GLEdgeView.RIGHT)) { + edgeView.onRelease(GLEdgeView.RIGHT); + } + } else if (mDeltaX > 0) { + edgeView.onPull(mDeltaX, y, GLEdgeView.RIGHT); + if (!edgeView.isFinished(GLEdgeView.LEFT)) { + edgeView.onRelease(GLEdgeView.LEFT); + } + } + + if (mDeltaY < 0) { + edgeView.onPull(-mDeltaY, x, GLEdgeView.TOP); + if (!edgeView.isFinished(GLEdgeView.BOTTOM)) { + edgeView.onRelease(GLEdgeView.BOTTOM); + } + } else if (mDeltaY > 0) { + edgeView.onPull(mDeltaY, x, GLEdgeView.BOTTOM); + if (!edgeView.isFinished(GLEdgeView.TOP)) { + edgeView.onRelease(GLEdgeView.TOP); + } + } + } + + private void getBottomState() { + List pages = mPages; + int bottom = mOffsetY; + int i = 0; + for (GalleryPageView page : pages) { + if (i != 0) { + bottom += mInterval; + } + bottom += page.getHeight(); + i++; + } + boolean hasNext = mIndex + pages.size() < mAdapter.size(); + + mBottomStateBottom = bottom; + mBottomStateHasNext = hasNext; + } + + // True for get top or bottom + private boolean scrollInternal(float dx, float dy, boolean fling, float x, float y) { + if (mPages.size() <= 0) { + return false; + } + + GalleryView galleryView = mGalleryView; + int width = galleryView.getWidth(); + int height = galleryView.getHeight(); + int pageWidth = (int) (width * mScale); + final float reservation = getReservation(); + boolean requestFill = false; + boolean result = false; + + int margin = pageWidth - width; + int dxInt = (int) dx; + if (margin > 0 && 0 != dxInt) { + int oldOffsetX = mOffsetX; + int exceptOffsetX = oldOffsetX - dxInt; + mOffsetX = MathUtils.clamp(exceptOffsetX, -margin, 0); + if (mOffsetX != oldOffsetX) { + requestFill = true; + } + // Do not show over scroll effect for left and right + /* + int extraOffsetX = mOffsetX - exceptOffsetX; + if (0 != extraOffsetX) { + overScrollEdge(extraOffsetX, 0, x, y); + } + */ + } + + int remainY = (int) dy; + while (remainY != 0) { + if (remainY < 0) { // Try to show top + int limit; + if (mIndex > 0) { + limit = (int) (-height * reservation) + mInterval; + } else { + limit = 0; + } + + if (mOffsetY - remainY <= limit) { + mOffsetY -= remainY; + remainY = 0; + requestFill = true; + mDeltaX = 0; + mDeltaY = 0; + } else { + if (mIndex > 0) { + mOffsetY = limit; + remainY = remainY + limit - mOffsetY; + // Offset one pixel to avoid infinite loop + ++mOffsetY; + ++remainY; + galleryView.forceFill(); + requestFill = false; + mDeltaX = 0; + mDeltaY = 0; + } else { + if (mOffsetY != limit) { + mOffsetY = limit; + requestFill = true; + } + if (!fling) { + overScrollEdge(0, remainY + limit - mOffsetY, x, y); + } + remainY = 0; + result = true; + } + } + } else { // Try to show bottom + getBottomState(); + int bottom = mBottomStateBottom; + boolean hasNext = mBottomStateHasNext; + + int limit; + if (hasNext) { + limit = (int) (height * (1 + reservation)) - mInterval; + } else { + limit = height; + } + // Fix limit for page not fill screen + limit = Math.min(bottom, limit); + + if (bottom - remainY >= limit) { + mOffsetY -= remainY; + remainY = 0; + requestFill = true; + mDeltaX = 0; + mDeltaY = 0; + } else { + if (hasNext) { + mOffsetY -= bottom - limit; + remainY = remainY + limit - bottom; + // Offset one pixel to avoid infinite loop + --mOffsetY; + --remainY; + galleryView.forceFill(); + requestFill = false; + mDeltaX = 0; + mDeltaY = 0; + } else { + if (mOffsetY != limit) { + mOffsetY -= bottom - limit; + requestFill = true; + } + if (!fling) { + overScrollEdge(0, remainY + limit - bottom, x, y); + } + remainY = 0; + result = true; + } + } + } + } + + if (requestFill) { + mGalleryView.requestFill(); + } + + return result; + } + + @Override + public void onScroll(float dx, float dy, float totalX, float totalY, float x, float y) { + mKeepTopPageIndex = GalleryPageView.INVALID_INDEX; + mKeepTop = INVALID_TOP; + mScrollUp = dy < 0; + scrollInternal(dx, dy, false, x, y); + } + + @Override + public void onFling(float velocityX, float velocityY) { + if (mPages.size() <= 0) { + return; + } + + mKeepTopPageIndex = GalleryPageView.INVALID_INDEX; + mKeepTop = INVALID_TOP; + mFlingUp = velocityY > 0; + + int maxX; + int minX; + int width = mGalleryView.getWidth(); + int pageWidth = (int) (width * mScale); + int margin = pageWidth - width; + if (margin > 0) { + maxX = -mOffsetX; + minX = -margin + mOffsetX; + } else { + maxX = 0; + minX = 0; + } + + int maxY; + if (mIndex > 0) { + maxY = Integer.MAX_VALUE; + } else { + maxY = -mOffsetY; + } + + getBottomState(); + int bottom = mBottomStateBottom; + boolean hasNext = mBottomStateHasNext; + int minY; + if (hasNext) { + minY = Integer.MIN_VALUE; + } else { + minY = mGalleryView.getHeight() - bottom; + } + + mPageFling.startFling((int) velocityX, minX, maxX, + (int) velocityY, minY, maxY); + } + + @Override + public boolean canScale() { + return mPages.size() > 0; + } + + @Override + public void onScale(float focusX, float focusY, float scale) { + if (mPages.isEmpty()) { + return; + } + + float oldScale = mScale; + mScale = MathUtils.clamp(oldScale * scale, MIN_SCALE, MAX_SCALE); + scale = mScale / oldScale; + + if (oldScale != mScale) { + GalleryPageView page = null; + // Keep scale page origin position + for (GalleryPageView p : mPages) { + if (p.bounds().top < focusY) { + page = p; + } else { + break; + } + } + + if (null != page) { + mKeepTopPageIndex = page.getIndex(); + mKeepTop = page.bounds().top; + + mGalleryView.forceFill(); + int oldKeepTop = mKeepTop; + mKeepTop = INVALID_TOP; + + // Apply scroll + int newOffsetX = (int) (focusX - ((focusX - mOffsetX) * scale)); + int newKeepTop; + if (page.isLoaded()) { + newKeepTop = (int) (focusY - ((focusY - oldKeepTop) * scale)); + } else { + newKeepTop = oldKeepTop; + } + scrollInternal(mOffsetX - newOffsetX, oldKeepTop - newKeepTop, false, focusX, focusY); + } else { + Log.e(TAG, "Can't find target page"); + mKeepTopPageIndex = GalleryPageView.INVALID_INDEX; + mKeepTop = INVALID_TOP; + mGalleryView.forceFill(); + } + } + } + + @Override + public boolean onUpdateAnimation(long time) { + boolean invalidate = mPageFling.calculate(time); + invalidate |= mSmoothScaler.calculate(time); + invalidate |= mOverScroller.calculate(time); + return invalidate; + } + + @Override + public void onDataChanged() { + AssertUtils.assertNotNull("The PagerLayoutManager is not attached", mAdapter); + + // Cancel all animations + cancelAllAnimations(); + // Remove all views + removeProgress(); + removeErrorView(); + removeAllPages(); + // Reset parameters + resetParameters(); + mGalleryView.requestFill(); + } + + @Override + public void onPageLeft() { + int size = mAdapter.size(); + if (size <= 0 || mPages.isEmpty()) { + return; + } + + /////// + // UP + /////// + GalleryView galleryView = mGalleryView; + if (mIndex == 0 && mOffsetY >= 0) { + mOverScroller.overScroll(GLEdgeView.TOP); + } else { + // Cancel all animations + cancelAllAnimations(); + + // Get first shown page + GalleryPageView previousPage = null; + GalleryPageView firstShownPage = null; + for (GalleryPageView p: mPages) { + if (isInScreen(p)) { + firstShownPage = p; + break; + } + previousPage = p; + } + + int height = galleryView.getHeight(); + int maxOffset = height - mInterval; + if (null == firstShownPage) { + Log.e(TAG, "Can't find first shown page when paging left"); + mOffsetY += height / 2; + } else { + int firstShownTop = firstShownPage.bounds().top; + if (firstShownTop >= 0) { + if (null == previousPage) { + Log.e(TAG, "Can't find previous page when paging left and offsetY == 0"); + mOffsetY += height / 2; + } else { + mOffsetY += Math.min(maxOffset, -previousPage.bounds().top); + } + } else { + mOffsetY += Math.min(maxOffset, -firstShownTop); + } + } + + // Request fill + mGalleryView.requestFill(); + } + } + + @Override + public void onPageRight() { + int size = mAdapter.size(); + if (size <= 0 || mPages.isEmpty()) { + return; + } + + ///////// + // DOWN + ///////// + GalleryView galleryView = mGalleryView; + getBottomState(); + int bottom = mBottomStateBottom; + boolean hasNext = mBottomStateHasNext; + if (!hasNext && bottom <= galleryView.getHeight()) { + mOverScroller.overScroll(GLEdgeView.BOTTOM); + } else { + // Cancel all animations + cancelAllAnimations(); + + // Get first shown page + GalleryPageView lastShownPage = null; + GalleryPageView nextPage = null; + for (GalleryPageView p: mPages) { + if (isInScreen(p)) { + lastShownPage = p; + } else if (null != lastShownPage) { + nextPage = p; + break; + } + } + + int height = galleryView.getHeight(); + int maxOffset = height - mInterval; + if (null == lastShownPage) { + Log.e(TAG, "Can't find last shown page when paging left"); + mOffsetY -= height / 2; + } else { + int lastShownBottom = lastShownPage.bounds().bottom; + if (lastShownBottom <= height) { + if (null == nextPage) { + Log.e(TAG, "Can't find previous page when paging left and offsetY == 0"); + mOffsetY -= height / 2; + } else { + mOffsetY -= Math.min(maxOffset, nextPage.bounds().bottom - height); + } + } else { + mOffsetY -= Math.min(maxOffset, lastShownBottom - height); + } + } + + // Request fill + mGalleryView.requestFill(); + } + } + + @Override + public boolean isTapOrPressEnable() { + return !mStopAnimationFinger; + } + + @Override + public GalleryPageView findPageByIndex(int index) { + for (GalleryPageView page : mPages) { + if (page.getIndex() == index) { + return page; + } + } + return null; + } + + @Override + public int getCurrentIndex() { + for (GalleryPageView page : mPages) { + if (isInScreen(page)) { + return page.getIndex(); + } + } + return GalleryPageView.INVALID_INDEX; + } + + @Override + public void setCurrentIndex(int index) { + int size = mAdapter.size(); + if (size <= 0) { + // Can't get size now, assume size is MAX + size = Integer.MAX_VALUE; + } + if (index < 0 || index >= size) { + return; + } + + mKeepTopPageIndex = index; + mKeepTop = INVALID_TOP; + + if (mPages.isEmpty()) { + mIndex = index; + } else { + // Fix the index page + GalleryPageView targetPage = null; + for (GalleryPageView page : mPages) { + if (page.getIndex() == index) { + targetPage =page; + break; + } + } + + if (targetPage != null) { + // Cancel all animations + cancelAllAnimations(); + mOffsetY -= targetPage.bounds().top; + // Request fill + mGalleryView.requestFill(); + } else { + mIndex = index; + mOffsetY = 0; + // Cancel all animations + cancelAllAnimations(); + // Remove all view + removeProgress(); + removeErrorView(); + removeAllPages(); + // Request fill + mGalleryView.requestFill(); + } + } + } + + @Override + public int getIndexUnder(float x, float y) { + if (mPages.isEmpty()) { + return GalleryPageView.INVALID_INDEX; + } else { + int intX = (int) x; + int intY = (int) y; + for (GalleryPageView page : mPages) { + if (page.bounds().contains(intX, intY)) { + return page.getIndex(); + } + } + return GalleryPageView.INVALID_INDEX; + } + } + + @Override + int getInternalCurrentIndex() { + int currentIndex = getCurrentIndex(); + if (currentIndex == GalleryPageView.INVALID_INDEX) { + currentIndex = mIndex; + } + return currentIndex; + } + + private class PageFling extends Fling { + + private int mVelocityX; + private int mVelocityY; + private int mDx; + private int mDy; + private int mLastX; + private int mLastY; + + public PageFling(Context context) { + super(context); + } + + public void startFling(int velocityX, int minX, int maxX, + int velocityY, int minY, int maxY) { + mVelocityX = velocityX; + mVelocityY = velocityY; + 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; + } + + if (mDx == 0 && mDy == 0) { + return; + } + + setDuration(Math.max(durationX, durationY)); + start(); + mGalleryView.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 (scrollInternal(-offsetX, -offsetY, true, 0, 0)) { + cancel(); + onFinish(); + } + mLastX = x; + mLastY = y; + } + + @Override + protected void onFinish() { + mFlingUp = false; + + int index = mIndex; + + boolean topEdge = index <= 0 && mOffsetY >= 0; + + getBottomState(); + int bottom = mBottomStateBottom; + boolean hasNext = mBottomStateHasNext; + boolean bottomEdge = !hasNext && bottom <= mGalleryView.getHeight(); + + if (topEdge && bottomEdge) { + return; + } + + GLEdgeView edgeView = mGalleryView.getEdgeView(); + if (topEdge && edgeView.isFinished(GLEdgeView.TOP)) { + edgeView.onAbsorb(mVelocityY, GLEdgeView.TOP); + } else if (bottomEdge && edgeView.isFinished(GLEdgeView.BOTTOM)) { + edgeView.onAbsorb(-mVelocityY, GLEdgeView.BOTTOM); + } + } + } + + private class SmoothScaler extends Animation { + + private float mFocusX; + private float mFocusY; + private float mStartScale; + private float mEndScale; + private float mLastScale; + + public SmoothScaler() { + setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR); + } + + public void startSmoothScaler(float focusX, float focusY, + float startScale, float endScale, int duration) { + mFocusX = focusX; + mFocusY = focusY; + mStartScale = startScale; + mEndScale = endScale; + mLastScale = startScale; + setDuration(duration); + start(); + mGalleryView.invalidate(); + } + + @Override + protected void onCalculate(float progress) { + if (mPages.size() <= 0) { + return; + } + + float scale = MathUtils.lerp(mStartScale, mEndScale, progress); + onScale(mFocusX, mFocusY, scale / mLastScale); + mLastScale = scale; + } + } + + private class OverScroller extends FloatAnimation { + + private int mDirection; + private int mPosition; + + public OverScroller() { + setDuration(300L); + } + + public void overScroll(int direction) { + mDirection = direction; + int range; + switch (mDirection) { + case GLEdgeView.LEFT: + case GLEdgeView.RIGHT: + range = mGalleryView.getWidth() / 7; + mPosition = mGalleryView.getHeight() / 2; + break; + case GLEdgeView.TOP: + case GLEdgeView.BOTTOM: + range = mGalleryView.getHeight() / 7; + mPosition = mGalleryView.getWidth() / 2; + break; + default: + return; + } + setRange(0, range); + start(); + mGalleryView.invalidate(); + } + + @Override + protected void onCalculate(float progress) { + super.onCalculate(progress); + mGalleryView.getEdgeView().onPull(get(), mPosition, mDirection); + } + + @Override + protected void onFinish() { + mGalleryView.getEdgeView().onRelease(mDirection); + } + } +} diff --git a/app/src/main/java/com/hippo/glgallery/SimpleAdapter.java b/app/src/main/java/com/hippo/glgallery/SimpleAdapter.java new file mode 100644 index 000000000..a4712302d --- /dev/null +++ b/app/src/main/java/com/hippo/glgallery/SimpleAdapter.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.glgallery; + +import androidx.annotation.NonNull; + +import com.hippo.glview.image.ImageTexture; +import com.hippo.glview.image.ImageWrapper; +import com.hippo.glview.view.GLRootView; + +public class SimpleAdapter extends GalleryView.Adapter implements GalleryProvider.Listener { + + private final GalleryProvider mProvider; + private final ImageTexture.Uploader mUploader; + private boolean mShowIndex = true; + + public SimpleAdapter(@NonNull GLRootView glRootView, @NonNull GalleryProvider provider) { + mProvider = provider; + mUploader = new ImageTexture.Uploader(glRootView); + } + + public void clearUploader() { + mUploader.clear(); + } + + public void setShowIndex(boolean show) { + mShowIndex = show; + } + + @Override + public void onBind(GalleryPageView view, int index) { + mProvider.request(index); + view.showInfo(); + view.setImage(null); + if (mShowIndex) { + view.setPage(index + 1); + } else { + view.hidePage(); + } + view.setProgress(GalleryPageView.PROGRESS_INDETERMINATE); + view.setError(null, null); + } + + @Override + public void onUnbind(GalleryPageView view, int index) { + mProvider.cancelRequest(index); + view.setImage(null); + view.setError(null, null); + } + + @Override + public String getError() { + return mProvider.getError(); + } + + @Override + public int size() { + return mProvider.size(); + } + + @Override + public void onDataChanged() { + mGalleryView.onDataChanged(); + } + + @Override + public void onPageWait(int index) { + GalleryPageView page = findPageByIndex(index); + if (page != null) { + page.showInfo(); + page.setImage(null); + if (mShowIndex) { + page.setPage(index + 1); + } else { + page.hidePage(); + } + page.setProgress(GalleryPageView.PROGRESS_INDETERMINATE); + page.setError(null, null); + } + } + + @Override + public void onPagePercent(int index, float percent) { + GalleryPageView page = findPageByIndex(index); + if (page != null) { + page.showInfo(); + page.setImage(null); + if (mShowIndex) { + page.setPage(index + 1); + } else { + page.hidePage(); + } + page.setProgress(percent); + page.setError(null, null); + } + } + + @Override + public void onPageSucceed(int index, ImageWrapper image) { + GalleryPageView page = findPageByIndex(index); + if (page != null) { + if (image.obtain()) { + ImageTexture imageTexture = new ImageTexture(image); + mUploader.addTexture(imageTexture); + page.showImage(); + page.setImage(imageTexture); + if (mShowIndex) { + page.setPage(index + 1); + } else { + page.hidePage(); + } + page.setProgress(GalleryPageView.PROGRESS_GONE); + page.setError(null, null); + } else { + // The image is recycled, request again. + // TODO request loop ? + mProvider.request(index); + } + } + } + + @Override + public void onPageFailed(int index, String error) { + GalleryPageView page = findPageByIndex(index); + if (page != null) { + page.showInfo(); + page.setImage(null); + if (mShowIndex) { + page.setPage(index + 1); + } else { + page.hidePage(); + } + page.setProgress(GalleryPageView.PROGRESS_GONE); + page.setError(error, mGalleryView); + } + } + + @Override + public void onDataChanged(int index) { + GalleryPageView page = findPageByIndex(index); + if (page != null) { + mProvider.request(index); + } + } + + private GalleryPageView findPageByIndex(int index) { + return mGalleryView != null ? mGalleryView.findPageByIndex(index) : null; + } +} diff --git a/app/src/main/java/com/hippo/glview/anim/AlphaAnimation.java b/app/src/main/java/com/hippo/glview/anim/AlphaAnimation.java new file mode 100644 index 000000000..38f4be23d --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.anim; + +import com.hippo.glview.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/glview/anim/Animation.java b/app/src/main/java/com/hippo/glview/anim/Animation.java new file mode 100644 index 000000000..913b962fe --- /dev/null +++ b/app/src/main/java/com/hippo/glview/anim/Animation.java @@ -0,0 +1,167 @@ +/* + * 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.glview.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 || mStartTime == ANIMATION_START; + } + + 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 void cancel() { + mStartTime = NO_ANIMATION; + } + + public void reset() { + 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); + + // It is ok to call cancel() in onCalculate() + if (mStartTime != NO_ANIMATION && elapse >= mDuration) { + mRunnedCount++; + if (mRunnedCount >= mRepeatCount && mRepeatCount != INFINITE) { + onFinish(); + mStartTime = NO_ANIMATION; + } else { + mStartTime += elapse; + } + } + + return mStartTime != NO_ANIMATION; + } + + abstract protected void onCalculate(float progress); + + protected void onFinish() { + } +} diff --git a/app/src/main/java/com/hippo/glview/anim/CanvasAnimation.java b/app/src/main/java/com/hippo/glview/anim/CanvasAnimation.java new file mode 100644 index 000000000..9ccfeea14 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.anim; + +import com.hippo.glview.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/glview/anim/FloatAnimation.java b/app/src/main/java/com/hippo/glview/anim/FloatAnimation.java new file mode 100644 index 000000000..1336ba8d1 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/annotation/RenderThread.java b/app/src/main/java/com/hippo/glview/annotation/RenderThread.java new file mode 100644 index 000000000..7410230ce --- /dev/null +++ b/app/src/main/java/com/hippo/glview/annotation/RenderThread.java @@ -0,0 +1,27 @@ +/* + * 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.glview.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +public @interface RenderThread { +} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/BasicTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/BasicTexture.java new file mode 100644 index 000000000..5dcd5975b --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/BasicTexture.java @@ -0,0 +1,221 @@ +/* + * 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.glview.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 { + + 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 final 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/glview/glrenderer/BitmapTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/BitmapTexture.java new file mode 100644 index 000000000..643f7a4fb --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/glrenderer/CanvasTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/CanvasTexture.java new file mode 100644 index 000000000..403d67d02 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/glrenderer/ColorTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/ColorTexture.java new file mode 100644 index 000000000..3519a4a89 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/glrenderer/ExtTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/ExtTexture.java new file mode 100644 index 000000000..69c98874b --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.glrenderer; + +// ExtTexture is a texture whose content comes from a external texture. +// Before drawing, setSize() should be called. +public class ExtTexture extends BasicTexture { + + private final 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/glview/glrenderer/FadeInTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/FadeInTexture.java new file mode 100644 index 000000000..8802889ce --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/FadeInTexture.java @@ -0,0 +1,52 @@ +/* + * 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.glview.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 { + + 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/glview/glrenderer/FadeOutTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/FadeOutTexture.java new file mode 100644 index 000000000..09c70e662 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/FadeOutTexture.java @@ -0,0 +1,52 @@ +/* + * 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.glview.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 { + + 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/glview/glrenderer/FadeTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/FadeTexture.java new file mode 100644 index 000000000..7cda34c06 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/FadeTexture.java @@ -0,0 +1,79 @@ +/* + * 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.glview.glrenderer; + +import com.hippo.glview.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 { + + // 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/glview/glrenderer/GLCanvas.java b/app/src/main/java/com/hippo/glview/glrenderer/GLCanvas.java new file mode 100644 index 000000000..3d5d36f4e --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/glrenderer/GLES11Canvas.java b/app/src/main/java/com/hippo/glview/glrenderer/GLES11Canvas.java new file mode 100644 index 000000000..567f94747 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/GLES11Canvas.java @@ -0,0 +1,1080 @@ +/* + * 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.glview.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.RectF; +import android.opengl.GLU; +import android.opengl.GLUtils; +import android.opengl.Matrix; +import android.util.Log; +import com.hippo.yorozuya.MathUtils; +import com.hippo.yorozuya.collect.IntList; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11Ext; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +public class GLES11Canvas implements GLCanvas { + @SuppressWarnings("unused") + private static final String TAG = "GLCanvasImp"; + + private static final float OPAQUE_ALPHA = 0.95f; + + 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 GL11 mGL; + + private final float mMatrixValues[] = new float[16]; + private final float mTextureMatrixValues[] = new float[16]; + + // The results of mapPoints are stored in this buffer, and the order is + // x1, y1, x2, y2. + private final float mMapPointsBuffer[] = new float[4]; + + private final float mTextureColor[] = new float[4]; + + private int mBoxCoords; + + private GLState mGLState; + private final ArrayList mTargetStack = new ArrayList(); + + private float mAlpha; + private final ArrayList mRestoreStack = new ArrayList(); + private ConfigState mRecycledRestoreAction; + + private final RectF mDrawTextureSourceRect = new RectF(); + private final RectF mDrawTextureTargetRect = new RectF(); + private final float[] mTempMatrix = new float[32]; + private final IntList mUnboundTextures = new IntList(); + private final IntList mDeleteBuffers = new IntList(); + private int mScreenWidth; + private int mScreenHeight; + private boolean mBlendEnabled = true; + private int mFrameBuffer[] = new int[1]; + private static float[] sCropRect = new float[4]; + + private RawTexture mTargetTexture; + + // Drawing statistics + int mCountDrawLine; + int mCountFillRect; + int mCountDrawMesh; + int mCountTextureRect; + int mCountTextureOES; + + private static GLId mGLId = new GLES11IdImpl(); + + public GLES11Canvas(GL11 gl) { + mGL = gl; + mGLState = new GLState(gl); + // First create an nio buffer, then create a VBO from it. + int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE; + FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0); + + int[] name = new int[1]; + mGLId.glGenBuffers(1, name, 0); + mBoxCoords = name[0]; + + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords); + gl.glBufferData(GL11.GL_ARRAY_BUFFER, xyBuffer.capacity() * (Float.SIZE / Byte.SIZE), + xyBuffer, GL11.GL_STATIC_DRAW); + + gl.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + // Enable the texture coordinate array for Texture 1 + gl.glClientActiveTexture(GL11.GL_TEXTURE1); + gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + gl.glClientActiveTexture(GL11.GL_TEXTURE0); + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + + // mMatrixValues and mAlpha will be initialized in setSize() + } + + @Override + public void setSize(int width, int height) { + if (mTargetTexture == null) { + mScreenWidth = width; + mScreenHeight = height; + } + mAlpha = 1.0f; + + GL11 gl = mGL; + gl.glViewport(0, 0, width, height); + gl.glMatrixMode(GL11.GL_PROJECTION); + gl.glLoadIdentity(); + GLU.gluOrtho2D(gl, 0, width, 0, height); + + gl.glMatrixMode(GL11.GL_MODELVIEW); + gl.glLoadIdentity(); + + float matrix[] = mMatrixValues; + Matrix.setIdentityM(matrix, 0); + // to match the graphic coordinate system in android, we flip it vertically. + if (mTargetTexture == null) { + Matrix.translateM(matrix, 0, 0, height, 0); + Matrix.scaleM(matrix, 0, 1, -1, 1); + } + } + + @Override + public void setAlpha(float alpha) { + mAlpha = alpha; + } + + @Override + public float getAlpha() { + return mAlpha; + } + + @Override + public void multiplyAlpha(float alpha) { + mAlpha *= alpha; + } + + private static ByteBuffer allocateDirectNativeOrderBuffer(int size) { + return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); + } + + @Override + public void drawRect(float x, float y, float width, float height, GLPaint paint) { + GL11 gl = mGL; + + mGLState.setColorMode(paint.getColor(), mAlpha); + mGLState.setLineWidth(paint.getLineWidth()); + + saveTransform(); + translate(x, y); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, COUNT_RECT_VERTEX); + + restoreTransform(); + mCountDrawLine++; + } + + @Override + public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) { + GL11 gl = mGL; + + mGLState.setColorMode(paint.getColor(), mAlpha); + mGLState.setLineWidth(paint.getLineWidth()); + + saveTransform(); + translate(x1, y1); + scale(x2 - x1, y2 - y1, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, COUNT_LINE_VERTEX); + + restoreTransform(); + 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()); + } + + @Override + public void fillRect(float x, float y, float width, float height, int color) { + mGLState.setColorMode(color, mAlpha); + GL11 gl = mGL; + + saveTransform(); + translate(x, y); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, COUNT_FILL_VERTEX); + + restoreTransform(); + mCountFillRect++; + } + + @Override + public void fillOval(float cx, float cy, float radiusX, float radiusY, int color) { + mGLState.setColorMode(color, mAlpha); + GL11 gl = mGL; + + saveTransform(); + translate(cx - radiusX, cy - radiusY); + scale(radiusX * 2, radiusY * 2, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_TRIANGLE_FAN, OFFSET_FILL_CIRCLE, OFFSET_LAST - OFFSET_FILL_CIRCLE); + + restoreTransform(); + 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) { + mGLState.setColorMode(color, mAlpha); + GL11 gl = mGL; + + saveTransform(); + translate(cx - radiusX, cy - radiusY); + scale(radiusX * 2, radiusY * 2, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_TRIANGLE_FAN, OFFSET_FILL_CIRCLE, OFFSET_LAST - OFFSET_FILL_CIRCLE - conjugateCount); + + restoreTransform(); + mCountFillRect++; + } + } + + @Override + public void translate(float x, float y, float z) { + Matrix.translateM(mMatrixValues, 0, 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) { + float[] m = mMatrixValues; + m[12] += m[0] * x + m[4] * y; + m[13] += m[1] * x + m[5] * y; + m[14] += m[2] * x + m[6] * y; + m[15] += m[3] * x + m[7] * y; + } + + @Override + public void scale(float sx, float sy, float sz) { + Matrix.scaleM(mMatrixValues, 0, sx, sy, sz); + } + + @Override + public void rotate(float angle, float x, float y, float z) { + if (angle == 0) return; + float[] temp = mTempMatrix; + Matrix.setRotateM(temp, 0, angle, x, y, z); + Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0); + System.arraycopy(temp, 16, mMatrixValues, 0, 16); + } + + @Override + public void multiplyMatrix(float matrix[], int offset) { + float[] temp = mTempMatrix; + Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset); + System.arraycopy(temp, 0, mMatrixValues, 0, 16); + } + + private void textureRect(float x, float y, float width, float height) { + GL11 gl = mGL; + + saveTransform(); + translate(x, y); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4); + + restoreTransform(); + mCountTextureRect++; + } + + @Override + public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, + int uvBuffer, int indexBuffer, int indexCount) { + float alpha = mAlpha; + if (!bindTexture(tex)) return; + + mGLState.setBlendEnabled(mBlendEnabled + && (!tex.isOpaque() || alpha < OPAQUE_ALPHA)); + mGLState.setTextureAlpha(alpha); + + // Reset the texture matrix. We will set our own texture coordinates + // below. + setTextureCoords(0, 0, 1, 1); + + saveTransform(); + translate(x, y); + + mGL.glLoadMatrixf(mMatrixValues, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, xyBuffer); + mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, uvBuffer); + mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + mGL.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); + mGL.glDrawElements(GL11.GL_TRIANGLE_STRIP, + indexCount, GL11.GL_UNSIGNED_BYTE, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords); + mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + restoreTransform(); + mCountDrawMesh++; + } + + // Transforms two points by the given matrix m. The result + // {x1', y1', x2', y2'} are stored in mMapPointsBuffer and also returned. + private float[] mapPoints(float m[], int x1, int y1, int x2, int y2) { + float[] r = mMapPointsBuffer; + + // Multiply m and (x1 y1 0 1) to produce (x3 y3 z3 w3). z3 is unused. + float x3 = m[0] * x1 + m[4] * y1 + m[12]; + float y3 = m[1] * x1 + m[5] * y1 + m[13]; + float w3 = m[3] * x1 + m[7] * y1 + m[15]; + r[0] = x3 / w3; + r[1] = y3 / w3; + + // Same for x2 y2. + float x4 = m[0] * x2 + m[4] * y2 + m[12]; + float y4 = m[1] * x2 + m[5] * y2 + m[13]; + float w4 = m[3] * x2 + m[7] * y2 + m[15]; + r[2] = x4 / w4; + r[3] = y4 / w4; + + return r; + } + + private void drawBoundTexture( + BasicTexture texture, int x, int y, int width, int height) { + // Test whether it has been rotated or flipped, if so, glDrawTexiOES + // won't work + if (isMatrixRotatedOrFlipped(mMatrixValues)) { + if (texture.hasBorder()) { + setTextureCoords( + 1.0f / texture.getTextureWidth(), + 1.0f / texture.getTextureHeight(), + (texture.getWidth() - 1.0f) / texture.getTextureWidth(), + (texture.getHeight() - 1.0f) / texture.getTextureHeight()); + } else { + setTextureCoords(0, 0, + (float) texture.getWidth() / texture.getTextureWidth(), + (float) texture.getHeight() / texture.getTextureHeight()); + } + textureRect(x, y, width, height); + } else { + // draw the rect from bottom-left to top-right + float points[] = mapPoints( + mMatrixValues, x, y + height, x + width, y); + x = (int) (points[0] + 0.5f); + y = (int) (points[1] + 0.5f); + width = (int) (points[2] + 0.5f) - x; + height = (int) (points[3] + 0.5f) - y; + if (width > 0 && height > 0) { + ((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height); + mCountTextureOES++; + } + } + } + + @Override + public void drawTexture( + BasicTexture texture, int x, int y, int width, int height) { + drawTexture(texture, x, y, width, height, mAlpha); + } + + private void drawTexture(BasicTexture texture, + int x, int y, int width, int height, float alpha) { + if (width <= 0 || height <= 0) return; + + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || alpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + mGLState.setTextureAlpha(alpha); + drawBoundTexture(texture, x, y, width, height); + } + + @Override + public void drawTexture(BasicTexture texture, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) return; + + // Copy the input to avoid changing it. + mDrawTextureSourceRect.set(source); + mDrawTextureTargetRect.set(target); + source = mDrawTextureSourceRect; + target = mDrawTextureTargetRect; + + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + convertCoordinate(source, target, texture); + setTextureCoords(source); + mGLState.setTextureAlpha(mAlpha); + textureRect(target.left, target.top, target.width(), target.height()); + } + + @Override + public void drawTexture(BasicTexture texture, float[] mTextureTransform, + int x, int y, int w, int h) { + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + setTextureCoords(mTextureTransform); + mGLState.setTextureAlpha(mAlpha); + textureRect(x, y, w, h); + } + + // 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; + } + } + + @Override + public void drawMixed(BasicTexture from, + int toColor, float ratio, int x, int y, int w, int h) { + drawMixed(from, toColor, ratio, x, y, w, h, mAlpha); + } + + private boolean bindTexture(BasicTexture texture) { + if (!texture.onBind(this)) return false; + int target = texture.getTarget(); + mGLState.setTextureTarget(target); + mGL.glBindTexture(target, texture.getId()); + return true; + } + + private void setTextureColor(float r, float g, float b, float alpha) { + float[] color = mTextureColor; + color[0] = r; + color[1] = g; + color[2] = b; + color[3] = alpha; + } + + private void setMixedColor(int toColor, float ratio, float alpha) { + // + // The formula we want: + // alpha * ((1 - ratio) * from + ratio * to) + // + // The formula that GL supports is in the form of: + // combo * from + (1 - combo) * to * scale + // + // So, we have combo = alpha * (1 - ratio) + // and scale = alpha * ratio / (1 - combo) + // + float combo = alpha * (1 - ratio); + float scale = alpha * ratio / (1 - combo); + + // Specify the interpolation factor via the alpha component of + // GL_TEXTURE_ENV_COLORs. + // RGB component are get from toColor and will used as SRC1 + float colorScale = scale * (toColor >>> 24) / (0xff * 0xff); + setTextureColor(((toColor >>> 16) & 0xff) * colorScale, + ((toColor >>> 8) & 0xff) * colorScale, + (toColor & 0xff) * colorScale, combo); + GL11 gl = mGL; + gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0); + + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA); + + // Wire up the interpolation factor for RGB. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA); + + // Wire up the interpolation factor for alpha. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA); + + } + + @Override + public void drawMixed(BasicTexture from, int toColor, float ratio, + RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) return; + + if (ratio <= 0.01f) { + drawTexture(from, source, target); + return; + } else if (ratio >= 1) { + fillRect(target.left, target.top, target.width(), target.height(), toColor); + return; + } + + float alpha = mAlpha; + + // Copy the input to avoid changing it. + mDrawTextureSourceRect.set(source); + mDrawTextureTargetRect.set(target); + source = mDrawTextureSourceRect; + target = mDrawTextureTargetRect; + + mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque() + || Color.alpha(toColor) != 255 || alpha < OPAQUE_ALPHA)); + + if (!bindTexture(from)) return; + + // Interpolate the RGB and alpha values between both textures. + mGLState.setTexEnvMode(GL11.GL_COMBINE); + setMixedColor(toColor, ratio, alpha); + convertCoordinate(source, target, from); + setTextureCoords(source); + textureRect(target.left, target.top, target.width(), target.height()); + mGLState.setTexEnvMode(GL11.GL_REPLACE); + } + + private void drawMixed(BasicTexture from, int toColor, + float ratio, int x, int y, int width, int height, float alpha) { + // change from 0 to 0.01f to prevent getting divided by zero below + if (ratio <= 0.01f) { + drawTexture(from, x, y, width, height, alpha); + return; + } else if (ratio >= 1) { + fillRect(x, y, width, height, toColor); + return; + } + + mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque() + || Color.alpha(toColor) != 255 || alpha < OPAQUE_ALPHA)); + + final GL11 gl = mGL; + if (!bindTexture(from)) return; + + // Interpolate the RGB and alpha values between both textures. + mGLState.setTexEnvMode(GL11.GL_COMBINE); + setMixedColor(toColor, ratio, alpha); + + drawBoundTexture(from, x, y, width, height); + mGLState.setTexEnvMode(GL11.GL_REPLACE); + } + + // TODO: the code only work for 2D should get fixed for 3D or removed + private static final int MSKEW_X = 4; + private static final int MSKEW_Y = 1; + private static final int MSCALE_X = 0; + private static final int MSCALE_Y = 5; + + private static boolean isMatrixRotatedOrFlipped(float matrix[]) { + final float eps = 1e-5f; + return Math.abs(matrix[MSKEW_X]) > eps + || Math.abs(matrix[MSKEW_Y]) > eps + || matrix[MSCALE_X] < -eps + || matrix[MSCALE_Y] > eps; + } + + private static class GLState { + + private final GL11 mGL; + + private int mTexEnvMode = GL11.GL_REPLACE; + private float mTextureAlpha = 1.0f; + private int mTextureTarget = GL11.GL_TEXTURE_2D; + private boolean mBlendEnabled = true; + private float mLineWidth = 1.0f; + private boolean mLineSmooth = false; + + public GLState(GL11 gl) { + mGL = gl; + + // Disable unused state + gl.glDisable(GL11.GL_LIGHTING); + + // Enable used features + gl.glEnable(GL11.GL_DITHER); + + gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + gl.glEnable(GL11.GL_TEXTURE_2D); + + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, + GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE); + + // Set the background color + gl.glClearColor(0f, 0f, 0f, 0f); + + gl.glEnable(GL11.GL_BLEND); + gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + + // We use 565 or 8888 format, so set the alignment to 2 bytes/pixel. + gl.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 2); + } + + public void setTexEnvMode(int mode) { + if (mTexEnvMode == mode) return; + mTexEnvMode = mode; + mGL.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, mode); + } + + public void setLineWidth(float width) { + if (mLineWidth == width) return; + mLineWidth = width; + mGL.glLineWidth(width); + } + + public void setTextureAlpha(float alpha) { + if (mTextureAlpha == alpha) return; + mTextureAlpha = alpha; + if (alpha >= OPAQUE_ALPHA) { + // The alpha is need for those texture without alpha channel + mGL.glColor4f(1, 1, 1, 1); + setTexEnvMode(GL11.GL_REPLACE); + } else { + mGL.glColor4f(alpha, alpha, alpha, alpha); + setTexEnvMode(GL11.GL_MODULATE); + } + } + + public void setColorMode(int color, float alpha) { + setBlendEnabled(Color.alpha(color) != 255 || alpha < OPAQUE_ALPHA); + + // Set mTextureAlpha to an invalid value, so that it will reset + // again in setTextureAlpha(float) later. + mTextureAlpha = -1.0f; + + setTextureTarget(0); + + float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f; + mGL.glColor4x( + Math.round(((color >> 16) & 0xFF) * prealpha), + Math.round(((color >> 8) & 0xFF) * prealpha), + Math.round((color & 0xFF) * prealpha), + Math.round(255 * prealpha)); + } + + // target is a value like GL_TEXTURE_2D. If target = 0, texturing is disabled. + public void setTextureTarget(int target) { + if (mTextureTarget == target) return; + if (mTextureTarget != 0) { + mGL.glDisable(mTextureTarget); + } + mTextureTarget = target; + if (mTextureTarget != 0) { + mGL.glEnable(mTextureTarget); + } + } + + public void setBlendEnabled(boolean enabled) { + if (mBlendEnabled == enabled) return; + mBlendEnabled = enabled; + if (enabled) { + mGL.glEnable(GL11.GL_BLEND); + } else { + mGL.glDisable(GL11.GL_BLEND); + } + } + } + + @Override + public void clearBuffer(float[] argb) { + if(argb != null && argb.length == 4) { + mGL.glClearColor(argb[1], argb[2], argb[3], argb[0]); + } else { + mGL.glClearColor(0, 0, 0, 1); + } + mGL.glClear(GL10.GL_COLOR_BUFFER_BIT); + } + + @Override + public void clearBuffer() { + clearBuffer(null); + } + + private void setTextureCoords(RectF source) { + setTextureCoords(source.left, source.top, source.right, source.bottom); + } + + private void setTextureCoords(float left, float top, + float right, float bottom) { + mGL.glMatrixMode(GL11.GL_TEXTURE); + mTextureMatrixValues[0] = right - left; + mTextureMatrixValues[5] = bottom - top; + mTextureMatrixValues[10] = 1; + mTextureMatrixValues[12] = left; + mTextureMatrixValues[13] = top; + mTextureMatrixValues[15] = 1; + mGL.glLoadMatrixf(mTextureMatrixValues, 0); + mGL.glMatrixMode(GL11.GL_MODELVIEW); + } + + private void setTextureCoords(float[] mTextureTransform) { + mGL.glMatrixMode(GL11.GL_TEXTURE); + mGL.glLoadMatrixf(mTextureTransform, 0); + mGL.glMatrixMode(GL11.GL_MODELVIEW); + } + + // unloadTexture and deleteBuffer can be called from the finalizer thread, + // so we synchronized on the mUnboundTextures object. + @Override + public boolean unloadTexture(BasicTexture t) { + synchronized (mUnboundTextures) { + if (!t.isLoaded()) return false; + mUnboundTextures.add(t.mId); + return true; + } + } + + @Override + public void deleteBuffer(int bufferId) { + synchronized (mUnboundTextures) { + mDeleteBuffers.add(bufferId); + } + } + + @Override + public void deleteRecycledResources() { + synchronized (mUnboundTextures) { + IntList ids = mUnboundTextures; + if (ids.size() > 0) { + mGLId.glDeleteTextures(mGL, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + + ids = mDeleteBuffers; + if (ids.size() > 0) { + mGLId.glDeleteBuffers(mGL, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + } + } + + @Override + public void save() { + save(SAVE_FLAG_ALL); + } + + @Override + public void save(int saveFlags) { + ConfigState config = obtainRestoreConfig(); + + if ((saveFlags & SAVE_FLAG_ALPHA) != 0) { + config.mAlpha = mAlpha; + } else { + config.mAlpha = -1; + } + + if ((saveFlags & SAVE_FLAG_MATRIX) != 0) { + System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16); + } else { + config.mMatrix[0] = Float.NEGATIVE_INFINITY; + } + + mRestoreStack.add(config); + } + + @Override + public void restore() { + if (mRestoreStack.isEmpty()) throw new IllegalStateException(); + ConfigState config = mRestoreStack.remove(mRestoreStack.size() - 1); + config.restore(this); + freeRestoreConfig(config); + } + + private void freeRestoreConfig(ConfigState action) { + action.mNextFree = mRecycledRestoreAction; + mRecycledRestoreAction = action; + } + + private ConfigState obtainRestoreConfig() { + if (mRecycledRestoreAction != null) { + ConfigState result = mRecycledRestoreAction; + mRecycledRestoreAction = result.mNextFree; + return result; + } + return new ConfigState(); + } + + private static class ConfigState { + float mAlpha; + float mMatrix[] = new float[16]; + ConfigState mNextFree; + + public void restore(GLES11Canvas canvas) { + if (mAlpha >= 0) canvas.setAlpha(mAlpha); + if (mMatrix[0] != Float.NEGATIVE_INFINITY) { + System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16); + } + } + } + + @Override + public void dumpStatisticsAndClear() { + String line = String.format( + "MESH:%d, TEX_OES:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", + mCountDrawMesh, mCountTextureRect, mCountTextureOES, + mCountFillRect, mCountDrawLine); + mCountDrawMesh = 0; + mCountTextureRect = 0; + mCountTextureOES = 0; + mCountFillRect = 0; + mCountDrawLine = 0; + Log.d(TAG, line); + } + + private void saveTransform() { + System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16); + } + + private void restoreTransform() { + System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16); + } + + private void setRenderTarget(RawTexture texture) { + GL11ExtensionPack gl11ep = (GL11ExtensionPack) mGL; + + if (mTargetTexture == null && texture != null) { + mGLId.glGenBuffers(1, mFrameBuffer, 0); + gl11ep.glBindFramebufferOES( + GL11ExtensionPack.GL_FRAMEBUFFER_OES, mFrameBuffer[0]); + } + if (mTargetTexture != null && texture == null) { + gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES, 0); + gl11ep.glDeleteFramebuffersOES(1, mFrameBuffer, 0); + } + + mTargetTexture = texture; + if (texture == null) { + setSize(mScreenWidth, mScreenHeight); + } else { + setSize(texture.getWidth(), texture.getHeight()); + + if (!texture.isLoaded()) texture.prepare(this); + + gl11ep.glFramebufferTexture2DOES( + GL11ExtensionPack.GL_FRAMEBUFFER_OES, + GL11ExtensionPack.GL_COLOR_ATTACHMENT0_OES, + GL11.GL_TEXTURE_2D, texture.getId(), 0); + + checkFramebufferStatus(gl11ep); + } + } + + @Override + public void endRenderTarget() { + RawTexture texture = mTargetStack.remove(mTargetStack.size() - 1); + setRenderTarget(texture); + restore(); // restore matrix and alpha + } + + @Override + public void beginRenderTarget(RawTexture texture) { + save(); // save matrix and alpha + mTargetStack.add(mTargetTexture); + setRenderTarget(texture); + } + + private static void checkFramebufferStatus(GL11ExtensionPack gl11ep) { + int status = gl11ep.glCheckFramebufferStatusOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES); + if (status != GL11ExtensionPack.GL_FRAMEBUFFER_COMPLETE_OES) { + String msg = ""; + switch (status) { + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES: + msg = "FRAMEBUFFER_FORMATS"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES: + msg = "FRAMEBUFFER_ATTACHMENT"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES: + msg = "FRAMEBUFFER_MISSING_ATTACHMENT"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_OES: + msg = "FRAMEBUFFER_DRAW_BUFFER"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_OES: + msg = "FRAMEBUFFER_READ_BUFFER"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_UNSUPPORTED_OES: + msg = "FRAMEBUFFER_UNSUPPORTED"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES: + msg = "FRAMEBUFFER_INCOMPLETE_DIMENSIONS"; + break; + } + throw new RuntimeException(msg + ":" + Integer.toHexString(status)); + } + } + + @Override + public void setTextureParameters(BasicTexture texture) { + int width = texture.getWidth(); + int height = texture.getHeight(); + // Define a vertically flipped crop rectangle for OES_draw_texture. + // The four values in sCropRect are: left, bottom, width, and + // height. Negative value of width or height means flip. + sCropRect[0] = 0; + sCropRect[1] = height; + sCropRect[2] = width; + sCropRect[3] = -height; + + // Set texture parameters. + int target = texture.getTarget(); + mGL.glBindTexture(target, texture.getId()); + mGL.glTexParameterfv(target, GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0); + mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE); + mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE); + mGL.glTexParameterf(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + mGL.glTexParameterf(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + } + + @Override + public void initializeTextureSize(BasicTexture texture, int format, int type) { + int target = texture.getTarget(); + mGL.glBindTexture(target, texture.getId()); + int width = texture.getTextureWidth(); + int height = texture.getTextureHeight(); + mGL.glTexImage2D(target, 0, format, width, height, 0, format, type, null); + } + + @Override + public void initializeTexture(BasicTexture texture, Bitmap bitmap) { + int target = texture.getTarget(); + mGL.glBindTexture(target, texture.getId()); + 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(); + mGL.glBindTexture(target, texture.getId()); + GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type); + } + + @Override + public int uploadBuffer(FloatBuffer buf) { + return uploadBuffer(buf, Float.SIZE / Byte.SIZE); + } + + @Override + public int uploadBuffer(ByteBuffer buf) { + return uploadBuffer(buf, 1); + } + + private int uploadBuffer(Buffer buf, int elementSize) { + int[] bufferIds = new int[1]; + mGLId.glGenBuffers(bufferIds.length, bufferIds, 0); + int bufferId = bufferIds[0]; + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, bufferId); + mGL.glBufferData(GL11.GL_ARRAY_BUFFER, buf.capacity() * elementSize, buf, + GL11.GL_STATIC_DRAW); + return bufferId; + } + + @Override + public void recoverFromLightCycle() { + // This is only required for GLES20 + } + + @Override + public void getBounds(Rect bounds, int x, int y, int width, int height) { + // This is only required for GLES20 + } + + @Override + public GLId getGLId() { + return mGLId; + } +} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/GLES11IdImpl.java b/app/src/main/java/com/hippo/glview/glrenderer/GLES11IdImpl.java new file mode 100644 index 000000000..05e9388a0 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/GLES11IdImpl.java @@ -0,0 +1,67 @@ +/* + * 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.glview.glrenderer; + +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +/** + * Open GL ES 1.1 implementation for generating and destroying texture IDs and + * buffer IDs + */ +public class GLES11IdImpl implements GLId { + + private static int sNextId = 1; + // Mutex for sNextId + private final static Object sLock = new Object(); + + @Override + public int generateTexture() { + synchronized (sLock) { + return sNextId++; + } + } + + @Override + public void glGenBuffers(int n, int[] buffers, int offset) { + synchronized (sLock) { + while (n-- > 0) { + buffers[offset + n] = sNextId++; + } + } + } + + @Override + public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) { + synchronized (sLock) { + gl.glDeleteTextures(n, textures, offset); + } + } + + @Override + public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) { + synchronized (sLock) { + gl.glDeleteBuffers(n, buffers, offset); + } + } + + @Override + public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) { + synchronized (sLock) { + gl11ep.glDeleteFramebuffersOES(n, buffers, offset); + } + } +} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/GLES20Canvas.java b/app/src/main/java/com/hippo/glview/glrenderer/GLES20Canvas.java new file mode 100644 index 000000000..3790688ff --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/GLES20Canvas.java @@ -0,0 +1,1039 @@ +/* + * 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.glview.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Color; +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.MathUtils; +import com.hippo.yorozuya.collect.IntList; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +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 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 final IntList mSaveFlags = new IntList(); + + private int mCurrentAlphaIndex = 0; + private int mCurrentMatrixIndex = 0; + + // Viewport size + private int mWidth; + private int mHeight; + + // Projection matrix + private final 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 final int mDrawProgram; + private final int mTextureProgram; + private final int mMeshProgram; + + // GL buffer containing BOX_COORDINATES + private final 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[] 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 IntList mUnboundTextures = new IntList(); + private final IntList mDeleteBuffers = new IntList(); + + // 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 final int[] mFrameBuffer = new int[1]; + + // Bound textures. + private final 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); + + mDrawProgram = assembleProgram(drawVertexShader, drawFragmentShader, mDrawParameters); + mTextureProgram = assembleProgram(textureVertexShader, textureFragmentShader, + mTextureParameters); + 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.removeAt(mSaveFlags.size() - 1); + 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 = mTextureParameters; + prepareTexture(texture, mTextureProgram, params); + 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 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); + + if (0 != Color.alpha(toColor)) { + 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) { + IntList 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(Locale.US, "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/glview/glrenderer/GLES20IdImpl.java b/app/src/main/java/com/hippo/glview/glrenderer/GLES20IdImpl.java new file mode 100644 index 000000000..787075517 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/GLES20IdImpl.java @@ -0,0 +1,42 @@ +package com.hippo.glview.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/glview/glrenderer/GLId.java b/app/src/main/java/com/hippo/glview/glrenderer/GLId.java new file mode 100644 index 000000000..dc578fe6b --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/glrenderer/GLPaint.java b/app/src/main/java/com/hippo/glview/glrenderer/GLPaint.java new file mode 100644 index 000000000..2f189997b --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.glrenderer; + +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) { + if (width < 0) { + throw new IllegalArgumentException("width < 0"); + } + mLineWidth = width; + } + + public float getLineWidth() { + return mLineWidth; + } +} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/MovableTextTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/MovableTextTexture.java new file mode 100644 index 000000000..916a36827 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/MovableTextTexture.java @@ -0,0 +1,183 @@ +package com.hippo.glview.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Typeface; + +import java.util.Arrays; + +// TODO support multiline +/** + * Works like movable type.
+ *
+ * Only support single line now + */ +public final class MovableTextTexture extends SpriteTexture { + + private final char[] mCharacters; + private final float[] mWidths; + private final float mHeight; + private final float mMaxWidth; + + private MovableTextTexture(Bitmap bitmap, int count, int[] rects, char[] characters, + float[] widths, float height, float maxWidth) { + super(bitmap, false, 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 MovableTextTexture 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); + + // Calculate 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 MovableTextTexture(bitmap, length, rects, characters, widths, height, maxWidth); + } +} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/MultiLineTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/MultiLineTexture.java new file mode 100644 index 000000000..d7600c892 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/glrenderer/NativeTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/NativeTexture.java new file mode 100644 index 000000000..e44a1dc2e --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/NativeTexture.java @@ -0,0 +1,103 @@ +/* + * 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.glview.glrenderer; + +import android.opengl.GLES20; +import android.util.Log; + +import javax.microedition.khronos.opengles.GL11; + +public abstract class NativeTexture extends BasicTexture { + + private static final String TAG = NativeTexture.class.getSimpleName(); + + private boolean mContentValid = true; + + private boolean mOpaque = true; + + public void invalidateContent() { + mContentValid = false; + } + + public static void checkError() { + int error = GLES20.glGetError(); + if (error != 0) { + Throwable t = new Throwable(); + Log.e(TAG, "GL error: " + error, t); + } + } + + protected abstract void texImage(boolean init); + + private void uploadToCanvas(GLCanvas canvas) { + // Get id + mId = canvas.getGLId().generateTexture(); + + // Prepare + canvas.setTextureParameters(this); + + // Call glTexImage2D + GLES20.glBindTexture(getTarget(), mId); + checkError(); + texImage(true); + checkError(); + + setAssociatedCanvas(canvas); + mState = STATE_LOADED; + mContentValid = true; + } + + public void updateContent(GLCanvas canvas) { + if (!isLoaded()) { + uploadToCanvas(canvas); + } else if (!mContentValid) { + // Call glTexSubImage2D + GLES20.glBindTexture(getTarget(), mId); + checkError(); + texImage(false); + checkError(); + mContentValid = true; + } + } + + /** + * Whether the content on GPU is valid. + */ + public boolean isContentValid() { + return isLoaded() && mContentValid; + } + + @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; + } +} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/NinePatchChunk.java b/app/src/main/java/com/hippo/glview/glrenderer/NinePatchChunk.java new file mode 100644 index 000000000..fb6bf3f64 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/glrenderer/NinePatchTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/NinePatchTexture.java new file mode 100644 index 000000000..bd5e1de25 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/glrenderer/RawTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/RawTexture.java new file mode 100644 index 000000000..0a5f7f2e9 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/glrenderer/ResourceTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/ResourceTexture.java new file mode 100644 index 000000000..613a32ac3 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.glrenderer; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +// 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) { + if (context == null) { + throw new IllegalArgumentException("context == null"); + } + 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/glview/glrenderer/SpriteTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/SpriteTexture.java new file mode 100644 index 000000000..c34acb786 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/SpriteTexture.java @@ -0,0 +1,73 @@ +package com.hippo.glview.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.RectF; + +import com.hippo.yorozuya.AssertUtils; + +public class SpriteTexture extends TiledTexture { + + private final int mCount; + private final int[] mRects; + + private final RectF mTempSource = new RectF(); + private final RectF mTempTarget = new RectF(); + + public SpriteTexture(Bitmap bitmap, boolean isOpaque, int count, int[] rects) { + super(bitmap, isOpaque); + + 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/glview/glrenderer/StringTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/StringTexture.java new file mode 100644 index 000000000..fc02ffeda --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/glrenderer/Texture.java b/app/src/main/java/com/hippo/glview/glrenderer/Texture.java new file mode 100644 index 000000000..c77c751aa --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/glrenderer/TextureUploader.java b/app/src/main/java/com/hippo/glview/glrenderer/TextureUploader.java new file mode 100644 index 000000000..b2f8818a8 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.glrenderer; + +import com.hippo.glview.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/glview/glrenderer/TiledTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/TiledTexture.java new file mode 100644 index 000000000..daacbbe38 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/TiledTexture.java @@ -0,0 +1,373 @@ +/* + * 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.glview.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.os.SystemClock; + +import com.hippo.glview.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 final Bitmap sUploadBitmap; + private static final Canvas sCanvas; + private static final Paint sBitmapPaint; + private static final 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(); + + static { + sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Bitmap.Config.ARGB_8888); + sCanvas = new Canvas(sUploadBitmap); + sBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + sBitmapPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + sPaint = new Paint(); + sPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + sPaint.setColor(Color.TRANSPARENT); + } + + 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, boolean isOpaque) { + 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)); + tile.setOpaque(isOpaque); + 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]); + } + } + } + + // 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. + @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 (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/glview/glrenderer/UploadedTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/UploadedTexture.java new file mode 100644 index 000000000..ea9a912ee --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/UploadedTexture.java @@ -0,0 +1,298 @@ +/* + * 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.glview.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.opengl.GLUtils; + +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 final HashMap sBorderLines = + new HashMap<>(); + private static final 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() { + if (mBitmap == null) { + throw new IllegalStateException("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(); + + if (bWidth > texWidth || bHeight > texHeight) { + throw new IllegalStateException("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/glview/glrenderer/XmlResourceTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/XmlResourceTexture.java new file mode 100644 index 000000000..c4c8989f2 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/glrenderer/XmlResourceTexture.java @@ -0,0 +1,80 @@ +/* + * 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.glview.glrenderer; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; + +import androidx.annotation.DrawableRes; +import androidx.core.content.ContextCompat; + +public class XmlResourceTexture extends UploadedTexture { + + private final Context mContext; + private final 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/glview/image/GLImageMovableTextView.java b/app/src/main/java/com/hippo/glview/image/GLImageMovableTextView.java new file mode 100644 index 000000000..73055d77e --- /dev/null +++ b/app/src/main/java/com/hippo/glview/image/GLImageMovableTextView.java @@ -0,0 +1,107 @@ +/* + * 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.glview.image; + +import android.graphics.Rect; +import android.text.TextUtils; + +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.glview.view.GLView; +import com.hippo.glview.view.Gravity; +import com.hippo.yorozuya.collect.CollectionUtils; + +public class GLImageMovableTextView extends GLView { + + ImageMovableTextTexture mTextTexture; + + private String mText = ""; + private int[] mIndexes = CollectionUtils.EMPTY_INT_ARRAY; + + private int mGravity = Gravity.NO_GRAVITY; + + private void generateIndexes() { + if (mTextTexture == null || TextUtils.isEmpty(mText)) { + mIndexes = CollectionUtils.EMPTY_INT_ARRAY; + } else { + mIndexes = mTextTexture.getTextIndexes(mText); + } + } + + public void setTextTexture(ImageMovableTextTexture textTexture) { + if (mTextTexture == textTexture) { + return; + } + mTextTexture = textTexture; + + generateIndexes(); + requestLayout(); + } + + public void setText(String text) { + if (text == null) { + text = ""; + } + if (text.equals(mText)) { + return; + } + mText = text; + + generateIndexes(); + requestLayout(); + } + + 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 + public void onRender(GLCanvas canvas) { + if (mTextTexture == null || mIndexes == null) { + return; + } + + 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/glview/image/ImageMovableTextTexture.java b/app/src/main/java/com/hippo/glview/image/ImageMovableTextTexture.java new file mode 100644 index 000000000..07a53901a --- /dev/null +++ b/app/src/main/java/com/hippo/glview/image/ImageMovableTextTexture.java @@ -0,0 +1,211 @@ +/* + * 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.glview.image; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Typeface; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.image.Image; + +import java.util.Arrays; + +public class ImageMovableTextTexture extends ImageSpriteTexture { + + private final char[] mCharacters; + private final float[] mWidths; + private final float mHeight; + private final float mMaxWidth; + + public ImageMovableTextTexture(@NonNull ImageWrapper image, int count, int[] rects, + char[] characters, float[] widths, float height, float maxWidth) { + super(image, 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 + */ + @Nullable + public static ImageMovableTextTexture 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); + + // Calculate 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; + } + } + + Image image = Image.create(bitmap); + bitmap.recycle(); + if (image == null) { + return null; + } + ImageWrapper imageWrapper = new ImageWrapper(image); + if (imageWrapper.obtain()) { + return new ImageMovableTextTexture(imageWrapper, length, rects, characters, widths, height, maxWidth); + } else { + return null; + } + } +} diff --git a/app/src/main/java/com/hippo/glview/image/ImageSpriteTexture.java b/app/src/main/java/com/hippo/glview/image/ImageSpriteTexture.java new file mode 100644 index 000000000..5b65d57cb --- /dev/null +++ b/app/src/main/java/com/hippo/glview/image/ImageSpriteTexture.java @@ -0,0 +1,91 @@ +/* + * 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.glview.image; + +import android.graphics.RectF; + +import androidx.annotation.NonNull; + +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.yorozuya.AssertUtils; + +public class ImageSpriteTexture extends ImageTexture { + + private final int mCount; + private final int[] mRects; + + private final RectF mTempSource = new RectF(); + private final RectF mTempTarget = new RectF(); + + public ImageSpriteTexture(@NonNull ImageWrapper image, int count, int[] rects) { + super(image); + + 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/glview/image/ImageTexture.java b/app/src/main/java/com/hippo/glview/image/ImageTexture.java new file mode 100644 index 000000000..d44371532 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/image/ImageTexture.java @@ -0,0 +1,705 @@ +/* + * 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.glview.image; + +import android.graphics.RectF; +import android.graphics.drawable.Animatable; +import android.os.Process; +import android.os.SystemClock; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; + +import com.hippo.glview.annotation.RenderThread; +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.glview.glrenderer.NativeTexture; +import com.hippo.glview.glrenderer.Texture; +import com.hippo.glview.view.GLRoot; +import com.hippo.yorozuya.thread.InfiniteThreadExecutor; +import com.hippo.yorozuya.thread.PVLock; +import com.hippo.yorozuya.thread.PriorityThreadFactory; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ImageTexture implements Texture, Animatable { + + @IntDef({TILE_SMALL, TILE_LARGE}) + @Retention(RetentionPolicy.SOURCE) + private @interface TileType {} + + private static final int TILE_SMALL = 0; + private static final int TILE_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 final Executor sThreadExecutor; + private static final PVLock sPVLock; + + private static Tile sSmallFreeTileHead = null; + private static Tile sLargeFreeTileHead = null; + private static final Object sFreeTileLock = new Object(); + + private final ImageWrapper 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 boolean mOpaque; + private final RectF mSrcRect = new RectF(); + private final RectF mDestRect = new RectF(); + + private boolean mImageBusy = false; + private final AtomicBoolean mRunning = new AtomicBoolean(); + private final AtomicBoolean mRequestAnimation = new AtomicBoolean(); + private final AtomicBoolean mFrameDirty = new AtomicBoolean(); + private final AtomicBoolean mNeedRelease = new AtomicBoolean(); + private final AtomicBoolean mReleased = new AtomicBoolean(); + + private Runnable mAnimateRunnable = null; + + private WeakReference mCallback; + + static { + sThreadExecutor = new InfiniteThreadExecutor(10 * 1000, new LinkedList(), + new PriorityThreadFactory("ImageTexture$AnimateTask", Process.THREAD_PRIORITY_BACKGROUND)); + sPVLock = new PVLock(3); + } + + 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 NativeTexture { + + @TileType + private int mTileType; + public int offsetX; + public int offsetY; + public ImageWrapper image; + public Tile nextFreeTile; + public int contentWidth; + public int contentHeight; + public int borderSize; + + public void setSize(@TileType int tileType, int width, int height) { + mTileType = tileType; + int tileSize; + if (tileType == TILE_SMALL) { + borderSize = SMALL_BORDER_SIZE; + tileSize = SMALL_TILE_SIZE; + } else if (tileType == TILE_LARGE) { + borderSize = LARGE_BORDER_SIZE; + tileSize = LARGE_TILE_SIZE; + } else { + throw new IllegalStateException("Not support tile type: " + tileType); + } + contentWidth = width; + contentHeight = height; + + mWidth = width + 2 * borderSize; + mHeight = height + 2 * borderSize; + mTextureWidth = tileSize; + mTextureHeight = tileSize; + } + + @Override + protected void texImage(boolean init) { + if (image != null && !image.isRecycled()) { + int w, h; + if (init) { + w = mTextureWidth; + h = mTextureHeight; + } else { + w = mWidth; + h = mHeight; + } + image.texImage(init, offsetX - borderSize, offsetY - borderSize, w, h); + } + } + + private void invalidate() { + invalidateContent(); + image = null; + } + + public void free() { + switch (mTileType) { + case TILE_SMALL: + freeSmallTile(this); + break; + case TILE_LARGE: + freeLargeTile(this); + break; + default: + throw new IllegalStateException("Not support tile type: " + mTileType); + } + } + + 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 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 class AnimateRunnable implements Runnable { + + public void doRun() { + long lastTime = System.nanoTime(); + long lastDelay = -1L; + + synchronized (mImage) { + // Check released, image busy, Need release + if (mReleased.get() || mImage.isImageRecycled() || mImageBusy || mNeedRelease.get()) { + mAnimateRunnable = null; + return; + } + // Obtain image + mImageBusy = true; + } + + if (!mImage.isCompleted()) { + try { + sPVLock.p(); + } catch (InterruptedException e) { + // Ignore + } + if (!mNeedRelease.get()) { + mImage.complete(); + } + sPVLock.v(); + } + + int frameCount = mImage.getFrameCount(); + + synchronized (mImage) { + // Release image + mImageBusy = false; + // Check need release, frameCount <= 1 + if (mNeedRelease.get() || frameCount <= 1) { + mAnimateRunnable = null; + return; + } + } + + if (mRequestAnimation.get()) { + mRunning.lazySet(true); + } + + for (;;) { + // Obtain + synchronized (mImage) { + // Check released, image busy, Need release, not running + if (mReleased.get() || mImage.isImageRecycled() || mImageBusy || mNeedRelease.get() || !mRunning.get()) { + mAnimateRunnable = null; + return; + } + // Obtain image + mImageBusy = true; + } + + mImage.advance(); + long delay = mImage.getDelay(); + long time = System.nanoTime(); + if (-1L != lastDelay) { + delay -= (time - lastTime) / 1000000 - lastDelay; + } + lastTime = time; + lastDelay = delay; + mFrameDirty.lazySet(true); + invalidateSelf(); + + synchronized (mImage) { + // Release image + mImageBusy = false; + } + + if (delay > 0) { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + // Ignore + } + } + } + } + + @Override + public void run() { + doRun(); + + while (mNeedRelease.get()) { + // Obtain + synchronized (mImage) { + // Check released + if (mReleased.get() || mImage.isImageRecycled()) { + break; + } + // Check image busy + if (mImageBusy) { + // Image is busy, means it is recycling + break; + } + // Obtain image + mImageBusy = true; + } + + if (!mReleased.get()) { + mImage.release(); + mReleased.lazySet(true); + } + + synchronized (mImage) { + // Release image + mImageBusy = false; + } + } + } + } + + /** + * Call {@link ImageWrapper#obtain()} first + */ + public ImageTexture(@NonNull ImageWrapper image) { + mImage = image; + int width = mWidth = image.getWidth(); + int height = mHeight = image.getHeight(); + boolean opaque = mOpaque = image.isOpaque(); + 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_CONTENT_SIZE) { + Tile tile = obtainSmallTile(); + tile.offsetX = x; + tile.offsetY = y; + tile.image = image; + tile.setSize(TILE_SMALL, w, Math.min(SMALL_CONTENT_SIZE, h)); + tile.setOpaque(opaque); + list.add(tile); + + int nextHeight = h - SMALL_CONTENT_SIZE; + if (nextHeight > 0) { + Tile nextTile = obtainSmallTile(); + nextTile.offsetX = x; + nextTile.offsetY = y + SMALL_CONTENT_SIZE; + nextTile.image = image; + nextTile.setSize(TILE_SMALL, w, nextHeight); + nextTile.setOpaque(opaque); + list.add(nextTile); + } + } else if (h <= SMALL_CONTENT_SIZE) { + Tile tile = obtainSmallTile(); + tile.offsetX = x; + tile.offsetY = y; + tile.image = image; + tile.setSize(TILE_SMALL, Math.min(SMALL_CONTENT_SIZE, w), h); + tile.setOpaque(opaque); + list.add(tile); + + int nextWidth = w - SMALL_CONTENT_SIZE; + if (nextWidth > 0) { + Tile nextTile = obtainSmallTile(); + nextTile.offsetX = x + SMALL_CONTENT_SIZE; + nextTile.offsetY = y; + nextTile.image = image; + nextTile.setSize(TILE_SMALL, nextWidth, h); + nextTile.setOpaque(opaque); + list.add(nextTile); + } + } else { + Tile tile = obtainLargeTile(); + tile.offsetX = x; + tile.offsetY = y; + tile.image = image; + tile.setSize(TILE_LARGE, w, h); + tile.setOpaque(opaque); + list.add(tile); + } + } + } + + mTiles = list.toArray(new Tile[list.size()]); + + if (!mImage.isCompleted()) { + Runnable runnable = new AnimateRunnable(); + mAnimateRunnable = runnable; + sThreadExecutor.execute(runnable); + } + } + + public final void setCallback(Callback cb) { + mCallback = new WeakReference<>(cb); + } + + public Callback getCallback() { + if (mCallback != null) { + return mCallback.get(); + } + return null; + } + + public void invalidateSelf() { + final Callback callback = getCallback(); + if (callback != null) { + callback.invalidateImageTexture(this); + } + } + + @Override + public void start() { + synchronized (mImage) { + if (!mImageBusy) { + mImageBusy = true; + } else { + mRequestAnimation.lazySet(true); + return; + } + } + + boolean end = mReleased.get() || mImage.isImageRecycled() || mNeedRelease.get() || + (mImage.isCompleted() && mImage.getFrameCount() <= 1) || mRunning.get(); + + synchronized (mImage) { + mImageBusy = false; + } + + if (end) { + return; + } + + mRunning.lazySet(true); + + synchronized (mImage) { + if (mAnimateRunnable == null) { + Runnable runnable = new AnimateRunnable(); + mAnimateRunnable = runnable; + sThreadExecutor.execute(runnable); + } + } + } + + @Override + public void stop() { + mRunning.lazySet(false); + mRequestAnimation.lazySet(false); + } + + @Override + public boolean isRunning() { + return mRunning.get(); + } + + 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; + } + + @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); + } + + @RenderThread + private void syncFrame() { + if (mFrameDirty.getAndSet(false)) { + // invalid tiles + for (Tile tile : mTiles) { + tile.invalidateContent(); + } + } + } + + @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; + + syncFrame(); + for (Tile t : mTiles) { + 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(); + + syncFrame(); + for (Tile t : mTiles) { + 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; + + syncFrame(); + for (Tile t : mTiles) { + 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(); + + syncFrame(); + for (Tile t : mTiles) { + 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 mOpaque; + } + + public boolean isReady() { + return mUploadIndex == mTiles.length; + } + + public void recycle() { + mRunning.lazySet(false); + + for (Tile mTile : mTiles) { + mTile.free(); + } + + boolean releaseNow; + + synchronized (mImage) { + if (!mImageBusy) { + releaseNow = true; + mImageBusy = true; + } else { + releaseNow = false; + mNeedRelease.lazySet(true); + } + } + + if (releaseNow) { + if (!mReleased.get()) { + mImage.release(); + mReleased.lazySet(true); + } + synchronized (mImage) { + mImageBusy = false; + } + } + } + + public interface Callback { + void invalidateImageTexture(ImageTexture who); + } +} diff --git a/app/src/main/java/com/hippo/glview/image/ImageWrapper.java b/app/src/main/java/com/hippo/glview/image/ImageWrapper.java new file mode 100644 index 000000000..d2f5ceb84 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/image/ImageWrapper.java @@ -0,0 +1,192 @@ +/* + * 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.glview.image; + +import android.graphics.Bitmap; +import android.graphics.Rect; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.hippo.image.Image; + +/** + * A wrapper for {@link Image}. It is useful for multi-usage. + * It handles image recycle automatically. + */ +public class ImageWrapper { + + private static final String LOG_TAG = "ImageWrapper"; + + private final Image mImage; + private final Rect mCut; + private int mReferences; + + /** + * Create ImageWrapper + * + * @param image the image should not be obtained or recycled. + */ + public ImageWrapper(@NonNull Image image) { + mImage = image; + mCut = new Rect(0, 0, image.getWidth(), image.getHeight()); + } + + /** + * Cuts this image to a specified region. + * If the region is out of the image size, clamp the region. + */ + public void setCutRect(int left, int top, int right, int bottom) { + mCut.left = Math.max(0, left); + mCut.top = Math.max(0, top); + mCut.right = Math.min(mImage.getWidth(), right); + mCut.bottom = Math.min(mImage.getHeight(), bottom); + + if (mCut.isEmpty()) { + // Empty mCut has unspecified behavior + Log.e(LOG_TAG, "Cut rect is empty"); + mCut.set(0, 0, mImage.getWidth(), mImage.getHeight()); + } + } + + /** + * Cuts this image to a specified region. + * The region is described in percent, {@code [0.0f, 1.0f]}. + * If the region is out of the image size, clamp the region. + */ + public void setCutPercent(float left, float top, float right, float bottom) { + setCutRect((int) (getWidth() * left), (int) (getHeight() * top), + (int) (getWidth() * right), (int) (getHeight() * bottom)); + } + + /** + * Obtain the image + * + * @return false for the image is recycled and obtain failed + */ + public synchronized boolean obtain() { + if (mImage.isRecycled()) { + return false; + } else { + ++mReferences; + return true; + } + } + + /** + * Release the image + */ + public synchronized void release() { + --mReferences; + if (mReferences <= 0 && !mImage.isRecycled()) { + mImage.recycle(); + } + } + + public boolean isImageRecycled() { + return mImage.isRecycled(); + } + + /** + * @see Image#getFormat() + */ + public int getFormat() { + return mImage.getFormat(); + } + + /** + * @see Image#getFormat() + */ + public int getWidth() { + return mCut.width(); + } + + /** + * @see Image#getHeight() + */ + public int getHeight() { + return mCut.height(); + } + + /** + * @see Image#complete() + */ + public boolean complete() { + synchronized (mImage) { + return mImage.complete(); + } + } + + /** + * @see Image#isCompleted() + */ + public boolean isCompleted() { + return mImage.isCompleted(); + } + + /** + * @see Image#render(int, int, Bitmap, int, int, int, int, boolean, int) + */ + public void render(int srcX, int srcY, Bitmap dst, int dstX, int dstY, + int width, int height, boolean fillBlank, int defaultColor) { + mImage.render(srcX + mCut.left, srcY + mCut.top, dst, dstX, dstY, + width, height, fillBlank, defaultColor); + } + + /** + * @see Image#texImage(boolean, int, int, int, int) + */ + public void texImage(boolean init, int offsetX, int offsetY, int width, int height) { + mImage.texImage(init, offsetX + mCut.left, offsetY + mCut.top, width, height); + } + + /** + * @see Image#advance() + */ + public void advance() { + mImage.advance(); + } + + /** + * @see Image#getDelay() + */ + public int getDelay() { + return mImage.getDelay(); + } + + /** + * @see Image#getFrameCount() + */ + public int getFrameCount() { + return mImage.getFrameCount(); + } + + /** + * @see Image#isOpaque() + */ + public boolean isOpaque() { + return mImage.isOpaque(); + } + + /** + * @see Image#isRecycled() + */ + public boolean isRecycled() { + return mImage.isRecycled(); + } +} diff --git a/app/src/main/java/com/hippo/glview/util/ApiHelper.java b/app/src/main/java/com/hippo/glview/util/ApiHelper.java new file mode 100644 index 000000000..ce8cb76f9 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/util/GalleryUtils.java b/app/src/main/java/com/hippo/glview/util/GalleryUtils.java new file mode 100644 index 000000000..48075cbc6 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/util/GalleryUtils.java @@ -0,0 +1,59 @@ +/* + * 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.glview.util; + +import android.graphics.Color; +import com.hippo.yorozuya.AssertError; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +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 final List sRenderThreads = new CopyOnWriteArrayList<>(); + + public static void setRenderThread() { + sRenderThreads.add(Thread.currentThread().hashCode()); + } + + public static boolean isRenderThread() { + return sRenderThreads.contains(Thread.currentThread().hashCode()); + } + + public static void assertInRenderThread() { + if (!isRenderThread()) { + throw new AssertError("Should not do this in non-render thread"); + } + } +} diff --git a/app/src/main/java/com/hippo/glview/util/MotionEventHelper.java b/app/src/main/java/com/hippo/glview/util/MotionEventHelper.java new file mode 100644 index 000000000..980020d3f --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/view/AnimationTime.java b/app/src/main/java/com/hippo/glview/view/AnimationTime.java new file mode 100644 index 000000000..eba37c949 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/view/GLRoot.java b/app/src/main/java/com/hippo/glview/view/GLRoot.java new file mode 100644 index 000000000..e582f5742 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.view; + +import android.content.Context; +import android.graphics.Matrix; + +import com.hippo.glview.anim.CanvasAnimation; +import com.hippo.glview.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/glview/view/GLRootView.java b/app/src/main/java/com/hippo/glview/view/GLRootView.java new file mode 100644 index 000000000..f2a116f46 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/view/GLRootView.java @@ -0,0 +1,751 @@ +/* + * 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.glview.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.PixelFormat; +import android.opengl.GLUtils; +import android.os.Build; +import android.os.Parcelable; +import android.os.Process; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.MotionEvent; +import android.view.SurfaceHolder; + +import androidx.annotation.NonNull; + +import com.hippo.glview.anim.CanvasAnimation; +import com.hippo.glview.glrenderer.BasicTexture; +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.glview.glrenderer.GLES11Canvas; +import com.hippo.glview.glrenderer.GLES20Canvas; +import com.hippo.glview.glrenderer.UploadedTexture; +import com.hippo.glview.util.ApiHelper; +import com.hippo.glview.util.GalleryUtils; +import com.hippo.glview.util.MotionEventHelper; +import com.hippo.tuxiang.EGLConfigChooser; +import com.hippo.tuxiang.EGLContextFactory; +import com.hippo.tuxiang.GLSurfaceView; +import com.hippo.tuxiang.Renderer; +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.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; + +// TODO Call attachToRoot and detachFromRoot in render thread +// 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 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 final 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 boolean mInDownState = false; + + private int mEGLContextClientVersion; + + private class GLRootRenderer implements Renderer { + + /** + * Called when the context is created, possibly after automatic destruction. + */ + @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 = mEGLContextClientVersion == 2 ? new GLES20Canvas() : new GLES11Canvas(gl); + 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. + */ + @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); + } + + @Override + public boolean onDrawFrame(GL10 gl) { + AnimationTime.update(); + + long t0 = System.nanoTime(); + + mRenderLock.lock(); + + while (mFreeze) { + mFreezeCondition.awaitUninterruptibly(); + } + + try { + onDrawFrameLocked(gl); + } finally { + mRenderLock.unlock(); + } + + long t = System.nanoTime(); + long duration = (t - t0) / 1000000; + + if (duration > 8) { + Log.v(TAG, "--- " + duration + " ---"); + } + + return true; + } + + @Override + public void onGLThreadStart() { } + + @Override + public void onGLThreadExit() { + if (mContentView != null && mContentView.isAttachedToRoot()) { + mContentView.detachFromRoot(); + } + } + + @Override + public void onGLThreadPause() { + if (mContentView != null) { + mContentView.pause(); + } + } + + @Override + public void onGLThreadResume() { + if (mContentView != null) { + mContentView.resume(); + } + } + } + + public GLRootView(Context context) { + this(context, null); + } + + @SuppressWarnings("deprecation") + public GLRootView(Context context, AttributeSet attrs) { + super(context, attrs); + mFlags |= FLAG_INITIALIZED; + setBackgroundDrawable(null); + + setEGLConfigChooser(new ConfigChooser()); + setEGLContextFactory(new ContextFactory()); + setRenderer(new GLRootRenderer()); + getHolder().setFormat(PixelFormat.RGB_888); + + // 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 final 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; + int compensation; + + // 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(); + } + + 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; + } + + 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(); + } + + /** + * {@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(); + } + } + + // Always chose a config + private class ConfigChooser implements EGLConfigChooser { + + private final int[] mValue = new int[1]; + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] num_config = new int[1]; + int[] configSpec = new int[] { EGL10.EGL_NONE }; + if (!egl.eglChooseConfig(display, configSpec, 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, configSpec, 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"); + } + + int renderableType = findConfigAttrib(egl, display, config, EGL10.EGL_RENDERABLE_TYPE, 0); + if ((renderableType & 0x0004) == 0x0004 /* EGL_OPENGL_ES2_BIT */) { + mEGLContextClientVersion = 2; + } else { + mEGLContextClientVersion = 1; + } + + return config; + } + + private EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { + // Use score to avoid "No config chosen" + int configIndex = 0; + int maxScore = 0; + + for (int i = 0, n = configs.length; i < n; i++) { + final EGLConfig config = configs[i]; + final int redSize = findConfigAttrib(egl, display, config, + EGL10.EGL_RED_SIZE, 0); + final int greenSize = findConfigAttrib(egl, display, config, + EGL10.EGL_GREEN_SIZE, 0); + final int blueSize = findConfigAttrib(egl, display, config, + EGL10.EGL_BLUE_SIZE, 0); + final int alphaSize = findConfigAttrib(egl, display, config, + EGL10.EGL_ALPHA_SIZE, 0); + final int sampleBuffers = findConfigAttrib(egl, display, config, + EGL10.EGL_SAMPLE_BUFFERS, 0); + final int samples = findConfigAttrib(egl, display, config, + EGL10.EGL_SAMPLES, 0); + final int depth = findConfigAttrib(egl, display, config, + EGL10.EGL_DEPTH_SIZE, 0); + final int stencil = findConfigAttrib(egl, display, config, + EGL10.EGL_STENCIL_SIZE, 0); + + final int score = redSize + greenSize + blueSize + alphaSize + + sampleBuffers + samples - depth - stencil; + + if (score > maxScore) { + maxScore = 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 class ContextFactory implements EGLContextFactory { + + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + @Override + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, + EGL10.EGL_NONE }; + + return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, + mEGLContextClientVersion != 0 ? attrib_list : null); + } + + @Override + public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { + if (!egl.eglDestroyContext(display, context)) { + Log.e("DefaultContextFactory", "display:" + display + " context: " + context); + throw new RuntimeException("eglDestroyContex failed: " + GLUtils.getEGLErrorString(egl.eglGetError())); + } + } + } +} diff --git a/app/src/main/java/com/hippo/glview/view/GLView.java b/app/src/main/java/com/hippo/glview/view/GLView.java new file mode 100644 index 000000000..2c1aec304 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/view/GLView.java @@ -0,0 +1,1367 @@ +/* + * 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.glview.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 androidx.annotation.IntDef; + +import com.hippo.glview.anim.CanvasAnimation; +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.glview.glrenderer.GLPaint; +import com.hippo.yorozuya.AssertUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +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; + + @IntDef({VISIBLE, INVISIBLE, GONE}) + @Retention(RetentionPolicy.SOURCE) + public @interface Visibility {} + + public static final int VISIBLE = 0b00000000; + public static final int INVISIBLE = 0b00000001; + public static final int GONE = 0b00000010; + public static final int VISIBILITY_INVALID = 0b00000011; + + private static final int FLAG_INVISIBLE = 0b00000011; + private static final int FLAG_SET_MEASURED_SIZE = 0b00000100; + private static final int FLAG_LAYOUT_REQUESTED = 0b00001000; + + /** + * 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[] mPositionInRoot = new int[2]; + protected final Rect mPaddings = new Rect(); + private final Rect mTempRect = 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; + + public void startAnimation(CanvasAnimation animation, boolean atOnce) { + GLRoot root = getGLRoot(); + if (root == null) throw new IllegalStateException(); + mAnimation = animation; + if (mAnimation != null) { + mAnimation.start(); + if (atOnce) { + mAnimation.setStartTime(AnimationTime.get()); + } else { + root.registerLaunchedAnimation(mAnimation); + } + } + invalidate(); + } + + /** + * Set the enabled state of this view. + * + * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}. + */ + public void setVisibility(@Visibility int visibility) { + @Visibility + int oldVisibility = getVisibility(); + if (visibility == oldVisibility) { + return; + } + + mViewFlags &= ~FLAG_INVISIBLE; + mViewFlags |= visibility; + + onVisibilityChanged(this, visibility); + + if (oldVisibility == GLView.GONE || visibility == GLView.GONE) { + requestLayout(); + } else { + invalidate(); + } + } + + /** + * Returns the visibility status for this view. + * + * @return One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}. + */ + @Visibility + @SuppressWarnings("WrongConstant") + public int getVisibility() { + int visibility = mViewFlags & FLAG_INVISIBLE; + + if (visibility == VISIBILITY_INVALID) { + visibility = VISIBLE; + mViewFlags &= ~FLAG_INVISIBLE; + mViewFlags |= visibility; + } + + return visibility; + } + + // This should only be called on the content pane (the topmost GLView). + 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(); + } + + // This should only be called on the content pane (the topmost GLView). + void pause() { + onPause(); + } + + // This should only be called on the content pane (the topmost GLView). + void resume() { + onResume(); + } + + public boolean isAttachedToRoot() { + return mRoot != null; + } + + // 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); + dispatchNotifyPositionInRoot(); + 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); + dispatchNotifyPositionInRoot(); + 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(); + } + } + + public 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) { + return; + } + + getValidRect(mTempRect); + if (mTempRect.isEmpty()) { + 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) { + notifyPositionInRoot(); + onLayout(sizeChanged, left, top, right, bottom); + } else { + dispatchNotifyPositionInRoot(); + } + mViewFlags &= ~FLAG_LAYOUT_REQUESTED; + } + + public void dispatchNotifyPositionInRoot() { + if (notifyPositionInRoot()) { + for (int i = 0, size = getComponentCount(); i < size; i++) { + getComponent(i).dispatchNotifyPositionInRoot(); + } + } + } + + public boolean notifyPositionInRoot() { + // Update position in root + int[] position = mPositionInRoot; + 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] += mBounds.left; + position[1] += mBounds.top; + } + int x = position[0]; + int y = position[1]; + if (x != oldX || y != oldY) { + onPositionInRootChanged(x, y, oldX, oldY); + return true; + } else { + return false; + } + } + + private boolean setBounds(int left, int top, int right, int bottom) { + Rect bounds = mBounds; + int oldW = bounds.right - bounds.left; + int oldH = bounds.bottom - bounds.top; + int newW = right - left; + int newH = bottom - top; + + boolean sizeChanged = oldW != newW || oldH != newH; + bounds.set(left, top, right, bottom); + + if (sizeChanged) { + onSizeChanged(newW, newH, oldW, oldH); + } + + return sizeChanged; + } + + protected void onSizeChanged(int newW, int newH, int oldW, int oldH) { + } + + 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] = mPositionInRoot[0]; + position[1] = mPositionInRoot[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 = mPositionInRoot[0]; + int y = mPositionInRoot[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); + if (component.getVisibility() == GONE) { + continue; + } + 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(GLView changedView, @Visibility int visibility) { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onVisibilityChanged(changedView, 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 onPause() { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onPause(); + } + } + + public void onResume() { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onResume(); + } + } + + 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/glview/view/Gravity.java b/app/src/main/java/com/hippo/glview/view/Gravity.java new file mode 100644 index 000000000..704a6f74a --- /dev/null +++ b/app/src/main/java/com/hippo/glview/view/Gravity.java @@ -0,0 +1,82 @@ +package com.hippo.glview.view; + +import androidx.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/glview/view/OrientationSource.java b/app/src/main/java/com/hippo/glview/view/OrientationSource.java new file mode 100644 index 000000000..4dd1978bc --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.view; + +public interface OrientationSource { + int getDisplayRotation(); + int getCompensation(); +} diff --git a/app/src/main/java/com/hippo/glview/view/TouchHelper.java b/app/src/main/java/com/hippo/glview/view/TouchHelper.java new file mode 100644 index 000000000..0685096cc --- /dev/null +++ b/app/src/main/java/com/hippo/glview/view/TouchHelper.java @@ -0,0 +1,243 @@ +/* + * 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.glview.view; + +import android.content.Context; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import androidx.annotation.NonNull; + +import com.hippo.yorozuya.LayoutUtils; +import com.hippo.yorozuya.SimpleHandler; + +public class TouchHelper { + + private final 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/glview/view/TouchOwner.java b/app/src/main/java/com/hippo/glview/view/TouchOwner.java new file mode 100644 index 000000000..b9b35a48b --- /dev/null +++ b/app/src/main/java/com/hippo/glview/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.glview.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/glview/widget/GLEdgeEffect.java b/app/src/main/java/com/hippo/glview/widget/GLEdgeEffect.java new file mode 100644 index 000000000..5b0d1c232 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/widget/GLEdgeEffect.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.glview.widget; + +import android.graphics.Rect; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.hippo.glview.glrenderer.GLCanvas; + +/** + * Material design edge effect + */ +public class GLEdgeEffect { + + // 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 GLEdgeEffect(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/glview/widget/GLEdgeView.java b/app/src/main/java/com/hippo/glview/widget/GLEdgeView.java new file mode 100644 index 000000000..51a35546c --- /dev/null +++ b/app/src/main/java/com/hippo/glview/widget/GLEdgeView.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.glview.widget; + +import android.opengl.Matrix; + +import androidx.annotation.IntDef; + +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.glview.view.GLView; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public final class GLEdgeView extends GLView { + + @IntDef({TOP, LEFT, BOTTOM, RIGHT}) + @Retention(RetentionPolicy.SOURCE) + private @interface Direction {} + + 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 final GLEdgeEffect[] mEffect = new GLEdgeEffect[4]; + private final float[] mMatrix = new float[4 * 16]; + + public GLEdgeView(int color) { + for (int i = 0; i < 4; i++) { + mEffect[i] = new GLEdgeEffect(color); + } + } + + public void setColor(int color) { + for (int i = 0; i < 4; i++) { + mEffect[i].setColor(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 render(GLCanvas 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) { + GLEdgeEffect 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(); + } + } + + public boolean isFinished(int direction) { + return mEffect[direction].isFinished(); + } +} diff --git a/app/src/main/java/com/hippo/glview/widget/GLFrameLayout.java b/app/src/main/java/com/hippo/glview/widget/GLFrameLayout.java new file mode 100644 index 000000000..23e0b9ae7 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/widget/GLFrameLayout.java @@ -0,0 +1,107 @@ +/* + * 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.glview.widget; + +import com.hippo.glview.view.GLView; +import com.hippo.glview.view.Gravity; + +public class GLFrameLayout 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); + if (component.getVisibility() == GONE) { + continue; + } + 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); + if (component.getVisibility() == GONE) { + continue; + } + 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/glview/widget/GLLinearLayout.java b/app/src/main/java/com/hippo/glview/widget/GLLinearLayout.java new file mode 100644 index 000000000..f1525c374 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/widget/GLLinearLayout.java @@ -0,0 +1,243 @@ +/* + * 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.glview.widget; + +import android.graphics.Rect; + +import androidx.annotation.IntDef; + +import com.hippo.glview.view.GLView; +import com.hippo.glview.view.Gravity; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +public class GLLinearLayout 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 final 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); + if (component.getVisibility() == GONE) { + continue; + } + 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); + if (component.getVisibility() == GONE) { + continue; + } + 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 (component.getVisibility() == GONE) { + continue; + } + + 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); + if (component.getVisibility() == GONE) { + continue; + } + 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); + if (component.getVisibility() == GONE) { + continue; + } + 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/glview/widget/GLMovableTextView.java b/app/src/main/java/com/hippo/glview/widget/GLMovableTextView.java new file mode 100644 index 000000000..9a8a99c48 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/widget/GLMovableTextView.java @@ -0,0 +1,108 @@ +/* + * 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.glview.widget; + +import android.graphics.Rect; +import android.text.TextUtils; + +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.glview.glrenderer.MovableTextTexture; +import com.hippo.glview.view.GLView; +import com.hippo.glview.view.Gravity; +import com.hippo.yorozuya.collect.CollectionUtils; + +public class GLMovableTextView extends GLView { + + MovableTextTexture mTextTexture; + + private String mText = ""; + private int[] mIndexes = CollectionUtils.EMPTY_INT_ARRAY; + + private int mGravity = Gravity.NO_GRAVITY; + + private void generateIndexes() { + if (mTextTexture == null || TextUtils.isEmpty(mText)) { + mIndexes = CollectionUtils.EMPTY_INT_ARRAY; + } else { + mIndexes = mTextTexture.getTextIndexes(mText); + } + } + + public void setTextTexture(MovableTextTexture textTexture) { + if (mTextTexture == textTexture) { + return; + } + mTextTexture = textTexture; + + generateIndexes(); + requestLayout(); + } + + public void setText(String text) { + if (text == null) { + text = ""; + } + if (text.equals(mText)) { + return; + } + mText = text; + + generateIndexes(); + requestLayout(); + } + + 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 + public void onRender(GLCanvas canvas) { + if (mTextTexture == null || mIndexes == null) { + return; + } + + 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/glview/widget/GLProgressView.java b/app/src/main/java/com/hippo/glview/widget/GLProgressView.java new file mode 100644 index 000000000..3a8467632 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/widget/GLProgressView.java @@ -0,0 +1,242 @@ +/* + * 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.glview.widget; + +import android.graphics.Color; +import android.graphics.Path; +import androidx.core.view.animation.PathInterpolatorCompat; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +import com.hippo.glview.anim.Animation; +import com.hippo.glview.anim.FloatAnimation; +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.glview.glrenderer.GLPaint; +import com.hippo.glview.view.AnimationTime; +import com.hippo.glview.view.GLView; + +import java.util.ArrayList; +import java.util.List; + +public class GLProgressView extends GLView { + + private static final Interpolator TRIM_START_INTERPOLATOR; + private static final Interpolator TRIM_END_INTERPOLATOR; + private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); + + static { + Path trimStartPath = new Path(); + trimStartPath.moveTo(0.0f, 0.0f); + trimStartPath.lineTo(0.5f, 0.0f); + trimStartPath.cubicTo(0.7f, 0.0f, 0.6f, 1f, 1f, 1f); + TRIM_START_INTERPOLATOR = PathInterpolatorCompat.create(trimStartPath); + + Path trimEndPath = new Path(); + trimEndPath.moveTo(0.0f, 0.0f); + trimEndPath.cubicTo(0.2f, 0.0f, 0.1f, 1f, 0.5f, 1f); + trimEndPath.lineTo(1f, 1f); + TRIM_END_INTERPOLATOR = PathInterpolatorCompat.create(trimEndPath); + } + + private final GLPaint mGLPaint; + + private float mCx; + private float mCy; + private float mRadiusX; + private float mRadiusY; + + private float mTrimStart = 0.0f; + private float mTrimEnd = 0.0f; + private float mTrimOffset = 0.0f; + private float mTrimRotation = 0.0f; + + private boolean mIndeterminate = false; + + private List mAnimations; + + public GLProgressView() { + mGLPaint = new GLPaint(); + mGLPaint.setColor(Color.WHITE); + mGLPaint.setBackgroundColor(Color.BLACK); + mAnimations = new ArrayList<>(); + + setupAnimations(); + } + + public void setupAnimations() { + FloatAnimation trimStart = new FloatAnimation() { + @Override + protected void onCalculate(float progress) { + super.onCalculate(progress); + mTrimStart = get(); + } + }; + trimStart.setRange(0.0f, 0.75f); + trimStart.setDuration(1333L); + trimStart.setInterpolator(TRIM_START_INTERPOLATOR); + trimStart.setRepeatCount(Animation.INFINITE); + + FloatAnimation trimEnd = new FloatAnimation() { + @Override + protected void onCalculate(float progress) { + super.onCalculate(progress); + mTrimEnd = get(); + } + }; + trimEnd.setRange(0.0f, 0.75f); + trimEnd.setDuration(1333L); + trimEnd.setInterpolator(TRIM_END_INTERPOLATOR); + trimEnd.setRepeatCount(Animation.INFINITE); + + FloatAnimation trimOffset = new FloatAnimation() { + @Override + protected void onCalculate(float progress) { + super.onCalculate(progress); + mTrimOffset = get(); + } + }; + trimOffset.setRange(0.0f, 0.25f); + trimOffset.setDuration(1333L); + trimOffset.setInterpolator(LINEAR_INTERPOLATOR); + trimOffset.setRepeatCount(Animation.INFINITE); + + FloatAnimation trimRotation = new FloatAnimation() { + @Override + protected void onCalculate(float progress) { + super.onCalculate(progress); + mTrimRotation = get(); + } + }; + trimRotation.setRange(0.0f, 720.0f); + trimRotation.setDuration(6665L); + trimRotation.setInterpolator(LINEAR_INTERPOLATOR); + trimRotation.setRepeatCount(Animation.INFINITE); + + mAnimations.add(trimStart); + mAnimations.add(trimEnd); + mAnimations.add(trimOffset); + mAnimations.add(trimRotation); + } + + private void startAnimations() { + List animations = mAnimations; + for (int i = 0, n = animations.size(); i < n; i++) { + animations.get(i).reset(); + } + } + + private void stopAnimations() { + List animations = mAnimations; + for (int i = 0, n = animations.size(); i < n; i++) { + animations.get(i).cancel(); + } + } + + @Override + protected void onLayout(boolean changeSize, int left, int top, int right, + int bottom) { + super.onLayout(changeSize, left, top, right, bottom); + + int width = right - left; + int height = bottom - top; + mGLPaint.setLineWidth(Math.min(width, height) / 12.0f); + mCx = width / 2; + mCy = height / 2; + mRadiusX = width / 48.0f * 19.0f; + mRadiusY = height / 48.0f * 19.0f; + } + + public void setColor(int color) { + mGLPaint.setColor(color); + invalidate(); + } + + public void setBgColor(int color) { + mGLPaint.setBackgroundColor(color); + invalidate(); + } + + public void setIndeterminate(boolean indeterminate) { + if (mIndeterminate != indeterminate) { + mIndeterminate = indeterminate; + if (indeterminate) { + startAnimations(); + } else { + stopAnimations(); + } + invalidate(); + } + } + + public boolean isIndeterminate() { + return mIndeterminate; + } + + public void setProgress(float progress) { + if (!mIndeterminate) { + mTrimStart = 0.0f; + mTrimEnd = progress; + mTrimOffset = 0.0f; + mTrimRotation = 0.0f; + invalidate(); + } + } + + @Override + public void onRender(GLCanvas canvas) { + update(); + + int width = getWidth(); + int height = getHeight(); + int cx = width / 2; + int cy = height / 2; + + float startAngle = (mTrimStart + mTrimOffset) * 360.0f - 90; + float sweepAngle = Math.max(12.0f, (mTrimEnd - mTrimStart) * 360.0f); + float rotation = mTrimRotation + startAngle; + + canvas.save(); + + canvas.translate(cx, cy); + canvas.rotate(rotation, 0, 0, 1); + if (rotation % 180 != 0) { + canvas.translate(-cy, -cx); + } else { + canvas.translate(-cx, -cy); + } + + canvas.drawArc(mCx, mCy, mRadiusX, mRadiusY, sweepAngle, mGLPaint); + + canvas.restore(); + } + + private void update() { + boolean invalidate = false; + + if (mIndeterminate) { + long currentTime = AnimationTime.get(); + List animations = mAnimations; + for (int i = 0, n = animations.size(); i < n; i++) { + invalidate |= animations.get(i).calculate(currentTime); + } + } + + if (invalidate) { + invalidate(); + } + } +} diff --git a/app/src/main/java/com/hippo/glview/widget/GLTextureView.java b/app/src/main/java/com/hippo/glview/widget/GLTextureView.java new file mode 100644 index 000000000..d24bfb153 --- /dev/null +++ b/app/src/main/java/com/hippo/glview/widget/GLTextureView.java @@ -0,0 +1,69 @@ +/* + * 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.glview.widget; + +import android.graphics.RectF; + +import com.hippo.glview.glrenderer.GLCanvas; +import com.hippo.glview.glrenderer.Texture; +import com.hippo.glview.view.GLView; + +public class GLTextureView extends GLView { + + private final RectF mSrc = new RectF(); + private final RectF mDst = new RectF(); + + private Texture mTexture; + + public void setTexture(Texture texture) { + mTexture = texture; + if (texture != null) { + mSrc.set(0.0f, 0.0f, texture.getWidth(), texture.getHeight()); + } else { + mSrc.setEmpty(); + } + invalidate(); + } + + public Texture getTexture() { + return mTexture; + } + + @Override + protected int getSuggestedMinimumWidth() { + return mTexture == null ? super.getSuggestedMinimumWidth() : mTexture.getWidth() + mPaddings.width(); + } + + @Override + protected int getSuggestedMinimumHeight() { + return mTexture == null ? super.getSuggestedMinimumWidth() : mTexture.getHeight() + mPaddings.height(); + } + + @Override + public void onRender(GLCanvas canvas) { + if (mTexture == null || mSrc.isEmpty()) { + return; + } + + mDst.set(mPaddings.left, mPaddings.top, getWidth() - mPaddings.right, getHeight() - mPaddings.bottom); + if (mDst.isEmpty()) { + return; + } + + mTexture.draw(canvas, mSrc, mDst); + } +}