/*
* Copyright (c) 2010, Sony Ericsson Mobile Communication AB. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of the Sony Ericsson Mobile Communication AB nor the names
* of its contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.geekband.luminous.homework.widget;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Camera;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LightingColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Adapter;
import android.widget.AdapterView;
import java.util.LinkedList;
/**
* A simple list view that displays the items as 3D blocks
*/
public class MyListView extends AdapterView<Adapter> {
/** Width of the items compared to the width of the list */
private static final float ITEM_WIDTH = 0.85f;
/** Space occupied by the item relative to the height of the item */
private static final float ITEM_VERTICAL_SPACE = 1.45f;
/** Ambient light intensity */
private static final int AMBIENT_LIGHT = 55;
/** Diffuse light intensity */
private static final int DIFFUSE_LIGHT = 200;
/** Specular light intensity */
private static final float SPECULAR_LIGHT = 70;
/** Shininess constant */
private static final float SHININESS = 200;
/** The max intensity of the light */
private static final int MAX_INTENSITY = 0xFF;
/** Amount of down scaling */
private static final float SCALE_DOWN_FACTOR = 0.15f;
/** Amount to rotate during one screen length */
private static final int DEGREES_PER_SCREEN = 270;
/** Represents an invalid child index */
private static final int INVALID_INDEX = -1;
/** Distance to drag before we intercept touch events */
private static final int TOUCH_SCROLL_THRESHOLD = 10;
/** Children added with this layout mode will be added below the last child */
private static final int LAYOUT_MODE_BELOW = 0;
/** Children added with this layout mode will be added above the first child */
private static final int LAYOUT_MODE_ABOVE = 1;
/** User is not touching the list */
private static final int TOUCH_STATE_RESTING = 0;
/** User is touching the list and right now it's still a "click" */
private static final int TOUCH_STATE_CLICK = 1;
/** User is scrolling the list */
private static final int TOUCH_STATE_SCROLL = 2;
/** The adapter with all the data */
private Adapter mAdapter;
/** Current touch state */
private int mTouchState = TOUCH_STATE_RESTING;
/** X-coordinate of the down event */
private int mTouchStartX;
/** Y-coordinate of the down event */
private int mTouchStartY;
/**
* The top of the first item when the touch down event was received
*/
private int mListTopStart;
/** The current top of the first item */
private int mListTop;
/**
* The offset from the top of the currently first visible item to the top of
* the first item
*/
private int mListTopOffset;
/** Current rotation */
private int mListRotation;
/** The adaptor position of the first visible item */
private int mFirstItemPosition;
/** The adaptor position of the last visible item */
private int mLastItemPosition;
/** A list of cached (re-usable) item views */
private final LinkedList<View> mCachedItemViews = new LinkedList<View>();
/** Used to check for long press actions */
private Runnable mLongPressRunnable;
/** Reusable rect */
private Rect mRect;
/** Camera used for 3D transformations */
private Camera mCamera;
/** Re-usable matrix for canvas transformations */
private Matrix mMatrix;
/** Paint object to draw with */
private Paint mPaint;
/** true if rotation of the items is enabled */
private boolean mRotationEnabled = true;
/** true if lighting of the items is enabled */
private boolean mLightEnabled = true;
/**
* Constructor
*
* @param context The context
* @param attrs Attributes
*/
public MyListView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
public void setAdapter(final Adapter adapter) {
mAdapter = adapter;
removeAllViewsInLayout();
requestLayout();
}
@Override
public Adapter getAdapter() {
return mAdapter;
}
@Override
public void setSelection(final int position) {
throw new UnsupportedOperationException("Not supported");
}
@Override
public View getSelectedView() {
throw new UnsupportedOperationException("Not supported");
}
/**
* Enables and disables individual rotation of the items.
*
* @param enable If rotation should be enabled or not
*/
public void enableRotation(final boolean enable) {
mRotationEnabled = enable;
if (!mRotationEnabled) {
mListRotation = 0;
}
invalidate();
}
/**
* Checks whether rotation is enabled
*
* @return true if rotation is enabled
*/
public boolean isRotationEnabled() {
return mRotationEnabled;
}
/**
* Enables and disables lighting of the items.
*
* @param enable If lighting should be enabled or not
*/
public void enableLight(final boolean enable) {
mLightEnabled = enable;
if (!mLightEnabled) {
mPaint.setColorFilter(null);
} else {
mPaint.setAlpha(0xFF);
}
invalidate();
}
/**
* Checks whether lighting is enabled
*
* @return true if rotation is enabled
*/
public boolean isLightEnabled() {
return mLightEnabled;
}
@Override
public boolean onInterceptTouchEvent(final MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startTouch(event);
return false;
case MotionEvent.ACTION_MOVE:
return startScrollIfNeeded(event);
default:
endTouch();
return false;
}
}
@Override
public boolean onTouchEvent(final MotionEvent event) {
if (getChildCount() == 0) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startTouch(event);
break;
case MotionEvent.ACTION_MOVE:
if (mTouchState == TOUCH_STATE_CLICK) {
startScrollIfNeeded(event);
}
if (mTouchState == TOUCH_STATE_SCROLL) {
scrollList((int)event.getY() - mTouchStartY);
}
break;
case MotionEvent.ACTION_UP:
if (mTouchState == TOUCH_STATE_CLICK) {
clickChildAt((int)event.getX(), (int)event.getY());
}
endTouch();
break;
default:
endTouch();
break;
}
return true;
}
@Override
protected void onLayout(final boolean changed, final int left, final int top, final int right,
final int bottom) {
super.onLayout(changed, left, top, right, bottom);
// if we don't have an adapter, we don't need to do anything
if (mAdapter == null) {
return;
}
if (getChildCount() == 0) {
mLastItemPosition = -1;
fillListDown(mListTop, 0);
} else {
final int offset = mListTop + mListTopOffset - getChildTop(getChildAt(0));
removeNonVisibleViews(offset);
fillList(offset);
}
positionItems();
invalidate();
}
@Override
protected boolean drawChild(final Canvas canvas, final View child, final long drawingTime) {
// get the bitmap
final Bitmap bitmap = child.getDrawingCache();
if (bitmap == null) {
// if the is null for some reason, default to the standard
// drawChild implementation
return super.drawChild(canvas, child, drawingTime);
}
// get top left coordinates
final int top = child.getTop();
final int left = child.getLeft();
// get centerX and centerY
final int childWidth = child.getWidth();
final int childHeight = child.getHeight();
final int centerX = childWidth / 2;
final int centerY = childHeight / 2;
// get scale
final float halfHeight = getHeight() / 2;
final float distFromCenter = (top + centerY - halfHeight) / halfHeight;
final float scale = (float)(1 - SCALE_DOWN_FACTOR * (1 - Math.cos(distFromCenter)));
// get rotation
float childRotation = mListRotation - 20 * distFromCenter;
childRotation %= 90;
if (childRotation < 0) {
childRotation += 90;
}
// draw the item
if (childRotation < 45) {
drawFace(canvas, bitmap, top, left, centerX, centerY, scale, childRotation - 90);
drawFace(canvas, bitmap, top, left, centerX, centerY, scale, childRotation);
} else {
drawFace(canvas, bitmap, top, left, centerX, centerY, scale, childRotation);
drawFace(canvas, bitmap, top, left, centerX, centerY, scale, childRotation - 90);
}
return false;
}
/**
* Draws a face of the 3D block
*
* @param canvas The canvas to draw on
* @param view A bitmap of the view to draw
* @param top Top placement of the view
* @param left Left placement of the view
* @param centerX Center x-coordinate of the view
* @param centerY Center y-coordinate of the view
* @param scale The scale to draw the view in
* @param rotation The rotation of the view
*/
private void drawFace(final Canvas canvas, final Bitmap view, final int top, final int left,
final int centerX, final int centerY, final float scale, final float rotation) {
// create the camera if we haven't before
if (mCamera == null) {
mCamera = new Camera();
}
// save the camera state
mCamera.save();
// translate and then rotate the camera
mCamera.translate(0, 0, centerY);
mCamera.rotateX(rotation);
mCamera.translate(0, 0, -centerY);
// create the matrix if we haven't before
if (mMatrix == null) {
mMatrix = new Matrix();
}
// get the matrix from the camera and then restore the camera
mCamera.getMatrix(mMatrix);
mCamera.restore();
// translate and scale the matrix
mMatrix.preTranslate(-centerX, -centerY);
mMatrix.postScale(scale, scale);
mMatrix.postTranslate(left + centerX, top + centerY);
// create and initialize the paint object
if (mPaint == null) {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setFilterBitmap(true);
}
// set the light
if (mLightEnabled) {
mPaint.setColorFilter(calculateLight(rotation));
//一个恐怖的反色矩阵
//mPaint.setColorFilter(new ColorMatrixColorFilter(new float[]{-1,0,0,0,255,0,-1,0,0,255,0,0,-1,0,255,0,0,0,1,0}));
//一个转灰度图的矩阵
//mPaint.setColorFilter(new ColorMatrixColorFilter(new float[]{0.3f,0.59f,0.11f,0,0,0.3f,0.59f,0.11f,0,0,0.3f,0.59f,0.11f,0,0,0,0,0,1,0}));
} else {
mPaint.setAlpha(0xFF - (int)(2 * Math.abs(rotation)));
}
// draw the bitmap
canvas.drawBitmap(view, mMatrix, mPaint);
}
/**
* Calculates the lighting of the item based on rotation.
*
* @param rotation The rotation of the item
* @return A color filter to use
*/
private LightingColorFilter calculateLight(final float rotation) {
final double cosRotation = Math.cos(Math.PI * rotation / 180);
int intensity = AMBIENT_LIGHT + (int)(DIFFUSE_LIGHT * cosRotation);
int highlightIntensity = (int)(SPECULAR_LIGHT * Math.pow(cosRotation, SHININESS));
if (intensity > MAX_INTENSITY) {
intensity = MAX_INTENSITY;
}
if (highlightIntensity > MAX_INTENSITY) {
highlightIntensity = MAX_INTENSITY;
}
final int light = Color.rgb(intensity, intensity, intensity);
final int highlight = Color.rgb(highlightIntensity, highlightIntensity, highlightIntensity);
return new LightingColorFilter(light, highlight);
}
/**
* Sets and initializes all things that need to when we start a touch
* gesture.
*
* @param event The down event
*/
private void startTouch(final MotionEvent event) {
// save the start place
mTouchStartX = (int)event.getX();
mTouchStartY = (int)event.getY();
mListTopStart = getChildTop(getChildAt(0)) - mListTopOffset;
// start checking for a long press
startLongPressCheck();
// we don't know if it's a click or a scroll yet, but until we know
// assume it's a click
mTouchState = TOUCH_STATE_CLICK;
}
/**
* Resets and recycles all things that need to when we end a touch gesture
*/
private void endTouch() {
// remove any existing check for longpress
removeCallbacks(mLongPressRunnable);
// reset touch state
mTouchState = TOUCH_STATE_RESTING;
}
/**
* Scrolls the list. Takes care of updating rotation (if enabled) and
* snapping
*
* @param scrolledDistance The distance to scroll
*/
private void scrollList(final int scrolledDistance) {
mListTop = mListTopStart + scrolledDistance;
if (mRotationEnabled) {
mListRotation = -(DEGREES_PER_SCREEN * mListTop) / getHeight();
}
requestLayout();
}
/**
* Posts (and creates if necessary) a runnable that will when executed call
* the long click listener
*/
private void startLongPressCheck() {
// create the runnable if we haven't already
if (mLongPressRunnable == null) {
mLongPressRunnable = new Runnable() {
public void run() {
if (mTouchState == TOUCH_STATE_CLICK) {
final int index = getContainingChildIndex(mTouchStartX, mTouchStartY);
if (index != INVALID_INDEX) {
longClickChild(index);
}
}
}
};
}
// then post it with a delay
postDelayed(mLongPressRunnable, ViewConfiguration.getLongPressTimeout());
}
/**
* Checks if the user has moved far enough for this to be a scroll and if
* so, sets the list in scroll mode
*
* @param event The (move) event
* @return true if scroll was started, false otherwise
*/
private boolean startScrollIfNeeded(final MotionEvent event) {
final int xPos = (int)event.getX();
final int yPos = (int)event.getY();
if (xPos < mTouchStartX - TOUCH_SCROLL_THRESHOLD
|| xPos > mTouchStartX + TOUCH_SCROLL_THRESHOLD
|| yPos < mTouchStartY - TOUCH_SCROLL_THRESHOLD
|| yPos > mTouchStartY + TOUCH_SCROLL_THRESHOLD) {
// we've moved far enough for this to be a scroll
removeCallbacks(mLongPressRunnable);
mTouchState = TOUCH_STATE_SCROLL;
return true;
}
return false;
}
/**
* Returns the index of the child that contains the coordinates given.
*
* @param x X-coordinate
* @param y Y-coordinate
* @return The index of the child that contains the coordinates. If no child
* is found then it returns INVALID_INDEX
*/
private int getContainingChildIndex(final int x, final int y) {
if (mRect == null) {
mRect = new Rect();
}
for (int index = 0; index < getChildCount(); index++) {
getChildAt(index).getHitRect(mRect);
if (mRect.contains(x, y)) {
return index;
}
}
return INVALID_INDEX;
}
/**
* Calls the item click listener for the child with at the specified
* coordinates
*
* @param x The x-coordinate
* @param y The y-coordinate
*/
private void clickChildAt(final int x, final int y) {
final int index = getContainingChildIndex(x, y);
if (index != INVALID_INDEX) {
final View itemView = getChildAt(index);
final int position = mFirstItemPosition + index;
final long id = mAdapter.getItemId(position);
performItemClick(itemView, position, id);
}
}
/**
* Calls the item long click listener for the child with the specified index
*
* @param index Child index
*/
private void longClickChild(final int index) {
final View itemView = getChildAt(index);
final int position = mFirstItemPosition + index;
final long id = mAdapter.getItemId(position);
final OnItemLongClickListener listener = getOnItemLongClickListener();
if (listener != null) {
listener.onItemLongClick(this, itemView, position, id);
}
}
/**
* Removes view that are outside of the visible part of the list. Will not
* remove all views.
*
* @param offset Offset of the visible area
*/
private void removeNonVisibleViews(final int offset) {
// We need to keep close track of the child count in this function. We
// should never remove all the views, because if we do, we loose track
// of were we are.
int childCount = getChildCount();
// if we are not at the bottom of the list and have more than one child
if (mLastItemPosition != mAdapter.getCount() - 1 && childCount > 1) {
// check if we should remove any views in the top
View firstChild = getChildAt(0);
while (firstChild != null && getChildBottom(firstChild) + offset < 0) {
// remove the top view
removeViewInLayout(firstChild);
childCount--;
mCachedItemViews.addLast(firstChild);
mFirstItemPosition++;
// update the list offset (since we've removed the top child)
mListTopOffset += getChildHeight(firstChild);
// Continue to check the next child only if we have more than
// one child left
if (childCount > 1) {
firstChild = getChildAt(0);
} else {
firstChild = null;
}
}
}
// if we are not at the top of the list and have more than one child
if (mFirstItemPosition != 0 && childCount > 1) {
// check if we should remove any views in the bottom
View lastChild = getChildAt(childCount - 1);
while (lastChild != null && getChildTop(lastChild) + offset > getHeight()) {
// remove the bottom view
removeViewInLayout(lastChild);
childCount--;
mCachedItemViews.addLast(lastChild);
mLastItemPosition--;
// Continue to check the next child only if we have more than
// one child left
if (childCount > 1) {
lastChild = getChildAt(childCount - 1);
} else {
lastChild = null;
}
}
}
}
/**
* Fills the list with child-views
*
* @param offset Offset of the visible area
*/
private void fillList(final int offset) {
final int bottomEdge = getChildBottom(getChildAt(getChildCount() - 1));
fillListDown(bottomEdge, offset);
final int topEdge = getChildTop(getChildAt(0));
fillListUp(topEdge, offset);
}
/**
* Starts at the bottom and adds children until we've passed the list bottom
*
* @param bottomEdge The bottom edge of the currently last child
* @param offset Offset of the visible area
*/
private void fillListDown(int bottomEdge, final int offset) {
while (bottomEdge + offset < getHeight() && mLastItemPosition < mAdapter.getCount() - 1) {
mLastItemPosition++;
final View newBottomchild = mAdapter.getView(mLastItemPosition, getCachedView(), this);
addAndMeasureChild(newBottomchild, LAYOUT_MODE_BELOW);
bottomEdge += getChildHeight(newBottomchild);
}
}
/**
* Starts at the top and adds children until we've passed the list top
*
* @param topEdge The top edge of the currently first child
* @param offset Offset of the visible area
*/
private void fillListUp(int topEdge, final int offset) {
while (topEdge + offset > 0 && mFirstItemPosition > 0) {
mFirstItemPosition--;
final View newTopCild = mAdapter.getView(mFirstItemPosition, getCachedView(), this);
addAndMeasureChild(newTopCild, LAYOUT_MODE_ABOVE);
final int childHeight = getChildHeight(newTopCild);
topEdge -= childHeight;
// update the list offset (since we added a view at the top)
mListTopOffset -= childHeight;
}
}
/**
* Adds a view as a child view and takes care of measuring it
*
* @param child The view to add
* @param layoutMode Either LAYOUT_MODE_ABOVE or LAYOUT_MODE_BELOW
*/
private void addAndMeasureChild(final View child, final int layoutMode) {
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
final int index = layoutMode == LAYOUT_MODE_ABOVE ? 0 : -1;
child.setDrawingCacheEnabled(true);
addViewInLayout(child, index, params, true);
final int itemWidth = (int)(getWidth() * ITEM_WIDTH);
child.measure(MeasureSpec.EXACTLY | itemWidth, MeasureSpec.UNSPECIFIED);
}
/**
* Positions the children at the "correct" positions
*/
private void positionItems() {
int top = mListTop + mListTopOffset;
for (int index = 0; index < getChildCount(); index++) {
final View child = getChildAt(index);
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
final int left = (getWidth() - width) / 2;
final int margin = getChildMargin(child);
final int childTop = top + margin;
child.layout(left, childTop, left + width, childTop + height);
top += height + 2 * margin;
}
}
/**
* Checks if there is a cached view that can be used
*
* @return A cached view or, if none was found, null
*/
private View getCachedView() {
if (mCachedItemViews.size() != 0) {
return mCachedItemViews.removeFirst();
}
return null;
}
/**
* Returns the margin of the child view taking into account the
* ITEM_VERTICAL_SPACE
*
* @param child The child view
* @return The margin of the child view
*/
private int getChildMargin(final View child) {
return (int)(child.getMeasuredHeight() * (ITEM_VERTICAL_SPACE - 1) / 2);
}
/**
* Returns the top placement of the child view taking into account the
* ITEM_VERTICAL_SPACE
*
* @param child The child view
* @return The top placement of the child view
*/
private int getChildTop(final View child) {
return child.getTop() - getChildMargin(child);
}
/**
* Returns the bottom placement of the child view taking into account the
* ITEM_VERTICAL_SPACE
*
* @param child The child view
* @return The bottom placement of the child view
*/
private int getChildBottom(final View child) {
return child.getBottom() + getChildMargin(child);
}
/**
* Returns the height of the child view taking into account the
* ITEM_VERTICAL_SPACE
*
* @param child The child view
* @return The height of the child view
*/
private int getChildHeight(final View child) {
return child.getMeasuredHeight() + 2 * getChildMargin(child);
}
}