CoverFlowOpenGL.java
/* * Copyright 2013 - Android Coverflow Gallery. (Vladyslav Yarovyi) * * 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.masterofcode.android.coverflow_library; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.PixelFormat; import android.graphics.RectF; import android.opengl.GLSurfaceView; import android.opengl.GLU; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.animation.AnimationUtils; import com.masterofcode.android.coverflow_library.listeners.CoverFlowListener; import com.masterofcode.android.coverflow_library.listeners.DataChangedListener; import com.masterofcode.android.coverflow_library.render_objects.Background; import com.masterofcode.android.coverflow_library.render_objects.CoverImage; import com.masterofcode.android.coverflow_library.render_objects.EmptyImage; import com.masterofcode.android.coverflow_library.utils.CoverflowQuery; import com.masterofcode.android.coverflow_library.utils.DataCache; import com.masterofcode.android.coverflow_library.utils.EQuality; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import java.util.ArrayList; import java.util.List; /** * Custom Cover Flow Gallery View. This core class is responsible for drawing * all images. * * @author skynet67 */ public class CoverFlowOpenGL extends GLSurfaceView implements GLSurfaceView.Renderer { public static final String TAG = "CoverFlowOpenGL"; // 滑动的最小像素偏移量 private static final int TOUCH_MINIMUM_MOVE = 5; // 动画阻力 private static final float FRICTION = 20.0f; // 10.f // 滑动的最大速度 private static final float MAX_SPEED = 5.0f; // 6.f // ------------------------------ private static final int COEF = 5; // 10 // 影响到滑动多少像素能切换到下一 tile private static final int OFFSET = 0; // 5 private static final float RCOEF = 0.25f; // ------------------------------ private int maxTiles = 21; // 缓存中总共可以容纳的 tiles private int visibleTiles = 4; // 除了中间,左侧/右侧最多可见 tiles 数 private int imageSize = 512; // 统一的图像大小(外框) private float mOffset; // 记录偏移了多少 tiles(当前中间tile的索引) //private int mLastOffset; //private RectF mTouchRect; // click 显示 toast 的范围 private int mWidth; private int mHeight; private boolean mTouchMoved; private float mTouchStartPos; private float mTouchStartX; // 按下起始的坐标 private float mTouchStartY; private float mStartOffset; // 起始 tile 的索引 private long mStartTime; private float mStartSpeed; private float mDuration; private Runnable mAnimationRunnable; private VelocityTracker mVelocity; private CoverFlowListener mListener; private DataCache<Integer, CoverImage> mCache; private CoverflowQuery aQuery; private List<String> imagesList; private List<CoverImage> images; private Activity mActivity; private EmptyImage emptyImage; private Background mBackground; // true:RGB_565 false:ARGB_8888 private boolean showBlackBars; public CoverFlowOpenGL(Context context) { super(context); init(); } public CoverFlowOpenGL(Context context, AttributeSet attrs) { super(context, attrs); init(); } public void setActivity(Activity activity) { this.mActivity = activity; aQuery = new CoverflowQuery(mActivity); } public void init() { setEGLConfigChooser(8, 8, 8, 8, 16, 0); setRenderer(this); // 设置渲染模式 setRenderMode(RENDERMODE_WHEN_DIRTY); getHolder().setFormat(PixelFormat.TRANSLUCENT); setZOrderMediaOverlay(true); setZOrderOnTop(true); // int cacheForVisibleTiles = (visibleTiles * 2 + 1) + 10; // // visible_left + center + visible_right + 10 additional mCache = new DataCache<Integer, CoverImage>(maxTiles); // Math.min(maxTiles,cacheForVisibleTiles)); // mLastOffset = 0; mOffset = 0; } public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glEnable(GL10.GL_TEXTURE_2D); // Enable Texture Mapping ( NEW ) gl.glShadeModel(GL10.GL_SMOOTH); // Enable Smooth Shading gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Black Background gl.glDisable(GL10.GL_DEPTH_TEST); // Enables Depth Testing // Really Nice Perspective Calculations gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); } public void onSurfaceChanged(GL10 gl, int w, int h) { mCache.clear(); mWidth = w; mHeight = h; if (mBackground != null) { mBackground.setGL(gl); mBackground.initBuffers(w, h); mBackground.loadGLTexture(); } if (emptyImage != null) { emptyImage.setGL(gl); emptyImage.setViewportData(w, h); emptyImage.setImageSize(imageSize); emptyImage.loadGLTexture(); } if (images != null && images.size() > 0) { for (CoverImage cImg : images) { if (cImg != null) { cImg.setGL(gl); cImg.setViewportData(w, h); cImg.removeTexture(); } } } // float imagew = w * RCOEF / 2.0f; // float imageh = h * RCOEF/ 2.0f; // 屏幕中心的一定范围,响应 click 事件 // mTouchRect = new RectF(w / 2 - imagew, h / 2 - imageh, w / 2 + imagew, // h / 2 + imageh); gl.glViewport(0, 0, w, h); // Reset The Current Viewport // 选择投影矩阵 gl.glMatrixMode(GL10.GL_PROJECTION); // 重置投影矩阵 gl.glLoadIdentity(); GLU.gluOrtho2D(gl, 0, w, 0, h); gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); // updateCache(); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: touchBegan(event); return true; case MotionEvent.ACTION_MOVE: touchMoved(event); return true; case MotionEvent.ACTION_UP: touchEnded(event); return true; } return false; } // 对 offset 的范围进行截断 private float checkValid(float off) { int max = imagesList.size() - 1; if (off < 0) return 0; else if (off > max) return max; return off; } private void touchBegan(MotionEvent event) { endAnimation(); float x = event.getX(); mTouchStartX = x; mTouchStartY = event.getY(); mStartTime = System.currentTimeMillis(); // 起始组件偏移 mStartOffset = mOffset; Log.e(TAG, "touchBegan mStartOffset: " + mStartOffset); mTouchMoved = false; // -2.5 ~ 2.5 mTouchStartPos = (x / mWidth) * COEF - OFFSET; mTouchStartPos /= 2; // ------- 统计滑动速度 ------ mVelocity = VelocityTracker.obtain(); mVelocity.addMovement(event); // ------- 统计滑动速度 ------ } private void touchMoved(MotionEvent event) { // -2.5 ~ 2.5 float pos = (event.getX() / mWidth) * COEF - OFFSET; pos /= 2; if (!mTouchMoved) { // 与按下那刻位置的像素偏移量 float dx = Math.abs(event.getX() - mTouchStartX); float dy = Math.abs(event.getY() - mTouchStartY); // 如果偏移很小,则不动 if (dx < TOUCH_MINIMUM_MOVE && dy < TOUCH_MINIMUM_MOVE) return; mTouchMoved = true; } // 更新组件偏移(注意是浮点数) mOffset = checkValid(mStartOffset + mTouchStartPos - pos); //Log.e(TAG, "touchMoved mOffset: " + mOffset); requestRender(); // ------- 统计滑动速度 ------ mVelocity.addMovement(event); // ------- 统计滑动速度 ------ } private void touchEnded(MotionEvent event) { float pos = (event.getX() / mWidth) * COEF - OFFSET; pos /= 2; if (mTouchMoved) { mStartOffset += mTouchStartPos - pos; mStartOffset = checkValid(mStartOffset); // 更新组件的偏移(注意是浮点数) mOffset = mStartOffset; // ------- 统计滑动速度 ------ mVelocity.addMovement(event); // 初始化速率单位 // 1000表示 1秒内运动了多少像素 mVelocity.computeCurrentVelocity(1000); double speed = mVelocity.getXVelocity(); speed = (speed / mWidth) * COEF; if (speed > MAX_SPEED) speed = MAX_SPEED; else if (speed < -MAX_SPEED) speed = -MAX_SPEED; // ------- 统计滑动速度 ------ // 开始一段时间的滑动动画 startAnimation(-speed); } else { // MainActivity中实现接口 // if (mTouchRect.contains(event.getX(), event.getY())) { // mListener.topTileClicked(this, (int) (mOffset + 0.01)); // } } } private void startAnimation(double speed) { if (mAnimationRunnable != null) return; double delta = speed * speed / (FRICTION * 2); // 如果反向滑动 if (speed < 0) delta = -delta; double nearest = mStartOffset + delta; // 取整 nearest = Math.floor(nearest + 0.5); nearest = checkValid((float) nearest); // 计算动画速度 mStartSpeed = (float) Math.sqrt(Math.abs(nearest - mStartOffset) * FRICTION * 2); // 反向动画速度 if (nearest < mStartOffset) mStartSpeed = -mStartSpeed; Log.i(TAG, "startAnimation! mStartSpeed: " + mStartSpeed); // 计算动画总时长 mDuration = Math.abs(mStartSpeed / FRICTION); // 记录动画起始时间 mStartTime = AnimationUtils.currentAnimationTimeMillis(); // 动画 Runnable对象 mAnimationRunnable = new Runnable() { public void run() { driveAnimation(); } }; // △ 继续执行driveAnimation post(mAnimationRunnable); } private void driveAnimation() { //Log.i(TAG, "driveAnimation! "); // 动画经过的时间 float elapsed = (AnimationUtils.currentAnimationTimeMillis() - mStartTime) / 1000.0f; // 如果超时则结束动画 if (elapsed >= mDuration) endAnimation(); else { // 根据经过的时间来刷新动画 updateAnimationAtElapsed(elapsed); // △ 继续执行本身 post(mAnimationRunnable); } } private void endAnimation() { if (mAnimationRunnable != null) { // 对组件的偏移进行取整(对齐组件的效果) mOffset = (float) Math.floor(mOffset + 0.5); mOffset = checkValid(mOffset); Log.i(TAG, "endAnimation: mOffset = " + mOffset); // 刷新动画 requestRender(); // △ 停止执行driveAnimation() removeCallbacks(mAnimationRunnable); mAnimationRunnable = null; // updateCache(); } } private void updateAnimationAtElapsed(float elapsed) { if (elapsed > mDuration) elapsed = mDuration; // 根据 起始动画速度 和 经过的时间 来计算当前的总偏移量 float delta = Math.abs(mStartSpeed) * elapsed - FRICTION * elapsed * elapsed / 2; if (mStartSpeed < 0) delta = -delta; // 注意是浮点数 mOffset = checkValid(mStartOffset + delta); //Log.i(TAG, "Update Anim: mOffset = " + mOffset); requestRender(); } // private void updateCache(){ // int diff = VISIBLE_TILES * 2 + 5; // int diffLeft = Math.max(0,(int)mOffset - diff); // int diffRight = Math.min(images.size(),(int)mOffset + diff); // // for(int i = 0; i < images.size(); i++){ // if(mCache.containsKey(i) && (i < diffLeft || i > diffRight)){ // mCache.removeObjectForKey(i); // } else // if(!mCache.containsKey(i) && i >= diffLeft && i <= diffRight){ // CoverImage img = images.get(i); // img.tryLoadTexture(dataChangedListener, i); // mCache.putObjectForKey(i, img); // } // } // } public int getMaxTiles() { return maxTiles; } public void setMaxTiles(int maxTiles) { this.maxTiles = maxTiles; mCache = new DataCache<Integer, CoverImage>(maxTiles); } public int getVisibleTiles() { return visibleTiles; } public void setVisibleTiles(int visibleTiles) { this.visibleTiles = visibleTiles; } public void setImageQuality(EQuality size) { imageSize = size.getValue(); } public void setImageShowBlackBars(boolean value) { showBlackBars = value; } public void setImagesList(List<String> imagesList) { this.imagesList = imagesList; if (imagesList != null && imagesList.size() > 0) { images = new ArrayList<CoverImage>(imagesList.size()); for (String imageUrl : imagesList) { CoverImage ci = new CoverImage(mActivity, aQuery) .setUrl(imageUrl).setImageSize(imageSize) .setShowBlackBars(showBlackBars); images.add(ci); } } } public void setCoverFlowListener(CoverFlowListener listener) { mListener = listener; } public void setSelection(int position) { endAnimation(); if (images != null && images.size() > 0) { position = Math.min(position, images.size() - 1); } mOffset = position; Log.w(TAG, "setSelection: mOffset = " + mOffset); requestRender(); } public void setBackgroundRes(int res) { mBackground = new Background(mActivity, res); } public void setEmptyRes(int res) { emptyImage = new EmptyImage(mActivity, res); } // public void setBackgroundUrl(String url){ // mBackground = new Background(mActivity, url); // } // public void setEmtpyUrl(String url){ // emptyImage = new EmptyImage(mActivity, url); // } public void onDrawFrame(GL10 gl) { gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); // clear Screen and Depth Buffer gl.glDisable(GL10.GL_DEPTH_TEST); gl.glClearColor(0, 0, 0, 0); gl.glClear(GL10.GL_COLOR_BUFFER_BIT); // Drawing gl.glTranslatef(0.0f, 0.0f, 0.0f); if (mBackground != null) { mBackground.draw(gl); } final float offset = mOffset; //Log.e(TAG, "onDrawFrame: offset = " + offset); int i; int max = imagesList != null ? imagesList.size() - 1 : 0; // 中间的最大 tile 的索引(取整) int mid = (int) Math.floor(offset + 0.5); // 可见的最左侧 tile 的索引(取整) int iStartPos = mid - visibleTiles; //Log.e(TAG, "onDrawFrame: iStartPos = " + iStartPos); if (iStartPos < 0) iStartPos = 0; // 绘制左侧 tiles for (i = iStartPos; i < mid; ++i) { drawTile(i, i - offset, gl); // 如果用 mid 就没有过渡特效 } // 可见的最右侧的 tile 的索引 int iEndPos = mid + visibleTiles; //Log.e(TAG, "onDrawFrame: iEndPos = " + iEndPos); // 绘制右侧和中间的 tiles if (iEndPos > max) iEndPos = max; for (i = iEndPos; i >= mid; --i) { drawTile(i, i - offset, gl); // 如果用 mid 就没有过渡特效 } // MainActivity中实现接口 // if (mLastOffset != (int) offset) { // mListener.tileOnTop(this, (int) offset); // mLastOffset = (int) offset; // //Log.e(TAG, "mLastOffset: " + offset); // } } private void drawTile( int position, // 当前的 tile float off, // 当前 tile 离 中间的 tile 的偏移 GL10 gl) { // ☆ 从键值对缓存中取出对应的 CoverImage ☆ // 不用每次都去下载 CoverImage cacheImg = mCache.objectForKey(position); boolean canDraw = false; if (cacheImg == null) { // 当前的 tile 所对应的图片 cacheImg = images.get(position); cacheImg.tryLoadTexture(dataChangedListener, position); // 添加到 键值对 缓存中 mCache.putObjectForKey(position, cacheImg); if (cacheImg.getTexture() != 0) { canDraw = true; } } else if (cacheImg.getTexture() != 0) { canDraw = true; } // 图像等比例缩放(还有稍稍偏移)后的大小 float desiredSize = canDraw ? cacheImg.getDesiredSize() : emptyImage .getDesiredSize(); // 一半宽度/一侧可见的tiles数 float spread = (mWidth - desiredSize) * 0.5f / visibleTiles; // 水平偏移 float trans = off * spread; // 根据离中心的距离,控制缩放比例 float sc = 1.0f - (Math.abs(off) / (visibleTiles + 1)); if (canDraw) { cacheImg.draw(gl, trans, sc); } else { emptyImage.draw(gl, trans, sc); } } // 定义监听器 private DataChangedListener dataChangedListener = new DataChangedListener() { public void imageUpdated(int position) { synchronized (this) { // 显示范围:(mOffset - visibleTiles, mOffset + visibleTiles) if (mOffset - visibleTiles < position || position < mOffset + visibleTiles) { requestRender(); } } } }; }