Inline hippo's glcomponents

* Replace android support with androidx as well
This commit is contained in:
tarsin committed 2022-05-30 09:20:02 +08:00
1 parent cf3769959f
commit 01daf4a63e
68 files changed
+16325 -7

No files matched your search

-7
View File
@@ -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'
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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<NotifyTask> 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<NotifyTask> mPool;
@Type
private int mType;
private int mIndex;
private float mPercent;
private ImageWrapper mImage;
private String mError;
public NotifyTask(Listener listener, ConcurrentPool<NotifyTask> 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<Integer, ImageWrapper> {
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);
}
}
File diff suppressed because it is too large. Load diff
@@ -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();
}
}
}
@@ -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();
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -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;
}
}
@@ -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);
}
}
@@ -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 <code>repeatCount</code> 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 <code>repeatCount</code> 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() {
}
}
@@ -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);
}
@@ -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;
}
}
@@ -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 {
}
@@ -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<BasicTexture, Object> sAllTextures
= new WeakHashMap<>();
private static final ThreadLocal<Class> 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);
}
}
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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;
}
}
@@ -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.
}
}
@@ -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);
}
}
}
@@ -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();
}
}
}
@@ -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();
}
}
@@ -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);
}
File diff suppressed because it is too large. Load diff
@@ -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);
}
}
}
File diff suppressed because it is too large. Load diff
@@ -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();
}
}
@@ -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);
}
@@ -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;
}
}
@@ -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.<br>
* <br>
* 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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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<NinePatchInstance> mInstanceCache
= new SmallCache<NinePatchInstance>();
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<V> {
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.
*
* <pre>
* 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
* </pre>
*
* @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;
}
}
}
@@ -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;
}
}
@@ -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();
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
@@ -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<UploadedTexture> mFgTextures =
new ArrayDeque<>(INIT_CAPACITY);
private final ArrayDeque<UploadedTexture> 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<UploadedTexture> 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;
}
}
}
@@ -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<TiledTexture> 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<TiledTexture> 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<Tile> 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;
}
}
@@ -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<BorderKey, Bitmap> 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();
}
}
@@ -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();
}
}
}
@@ -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);
}
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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<Callback> mCallback;
static {
sThreadExecutor = new InfiniteThreadExecutor(10 * 1000, new LinkedList<Runnable>(),
new PriorityThreadFactory("ImageTexture$AnimateTask", Process.THREAD_PRIORITY_BACKGROUND));
sPVLock = new PVLock(3);
}
public static class Uploader implements GLRoot.OnGLIdleListener {
private final ArrayDeque<ImageTexture> 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<ImageTexture> 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<Tile> 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);
}
}
@@ -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();
}
}
@@ -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;
}
}
}
@@ -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<Integer> 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");
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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();
}
@@ -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 <code>GLView</code>s. 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
// <code>GLRootView</code> 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<CanvasAnimation> mAnimations =
new ArrayList<>();
private final ArrayDeque<OnGLIdleListener> 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<Parcelable> container) {
super.dispatchSaveInstanceState(container);
if (mContentView != null) {
mContentView.saveHierarchyState(container);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void dispatchRestoreInstanceState(@NonNull SparseArray<Parcelable> 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()));
}
}
}
}
File diff suppressed because it is too large. Load diff
@@ -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;
}
}
}
}
@@ -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();
}
@@ -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);
}
}
}
@@ -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();
}
@@ -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.
*
* <p>Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement
* of the pull point is known.</p>
*
* @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.
*
* <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
* the method <code>getCurrVelocity</code> will provide a reasonable approximation
* to use here.</p>
*
* @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;
}
}
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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<GLView> 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);
}
}
}
@@ -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);
}
}
@@ -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<Animation> 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<Animation> animations = mAnimations;
for (int i = 0, n = animations.size(); i < n; i++) {
animations.get(i).reset();
}
}
private void stopAnimations() {
List<Animation> 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<Animation> animations = mAnimations;
for (int i = 0, n = animations.size(); i < n; i++) {
invalidate |= animations.get(i).calculate(currentTime);
}
}
if (invalidate) {
invalidate();
}
}
}
@@ -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);
}
}