/* * Copyright (c) 2014. Marshal Chen. */ package com.marshalchen.common.uimodule.fancycoverflow; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.graphics.*; import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; /** * This class has only internal use (package scope). * <p/> * It is responsible for applying additional effects to each coverflow item, that can only be applied at view level * (e.g. color saturation). * <p/> * This is a ViewGroup by intention to enable child views in layouts to stay interactive (like buttons) though * transformed. * <p/> * Since this class is only used within the FancyCoverFlowAdapter it doesn't need to check if there are multiple * children or not (there can only be one at all times). */ @SuppressWarnings("ConstantConditions") class FancyCoverFlowItemWrapper extends ViewGroup { // ============================================================================= // Private members // ============================================================================= private float saturation; private boolean isReflectionEnabled = false; private float imageReflectionRatio; private int reflectionGap; private float originalScaledownFactor; /** * This is a matrix to apply color filters (like saturation) to the wrapped view. */ private ColorMatrix colorMatrix; /** * This paint is used to draw the wrapped view including any filters. */ private Paint paint; /** * This is a cache holding the wrapped view's visual representation. */ private Bitmap wrappedViewBitmap; /** * This canvas is used to let the wrapped view draw it's content. */ private Canvas wrappedViewDrawingCanvas; // ============================================================================= // Constructor // ============================================================================= public FancyCoverFlowItemWrapper(Context context) { super(context); this.init(); } public FancyCoverFlowItemWrapper(Context context, AttributeSet attrs) { super(context, attrs); this.init(); } public FancyCoverFlowItemWrapper(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); this.init(); } private void init() { this.paint = new Paint(); this.colorMatrix = new ColorMatrix(); // TODO: Define a default value for saturation inside an XML. this.setSaturation(1); } // ============================================================================= // Getters / Setters // ============================================================================= void setReflectionEnabled(boolean hasReflection) { if (hasReflection != this.isReflectionEnabled) { this.isReflectionEnabled = hasReflection; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { // Turn off hardware acceleration if necessary (reflections won't support it). this.setLayerType(hasReflection ? View.LAYER_TYPE_SOFTWARE : View.LAYER_TYPE_HARDWARE, null); } this.remeasureChildren(); } } void setReflectionRatio(float imageReflectionRatio) { if (imageReflectionRatio != this.imageReflectionRatio) { this.imageReflectionRatio = imageReflectionRatio; this.remeasureChildren(); } } void setReflectionGap(int reflectionGap) { if (reflectionGap != this.reflectionGap) { this.reflectionGap = reflectionGap; this.remeasureChildren(); } } public void setSaturation(float saturation) { if (saturation != this.saturation) { this.saturation = saturation; this.colorMatrix.setSaturation(saturation); this.paint.setColorFilter(new ColorMatrixColorFilter(this.colorMatrix)); } } // ============================================================================= // Supertype overrides // ============================================================================= @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); this.remeasureChildren(); // If we have reflection enabled, the original image is scaled down and a reflection is added beneath. Thus, // while maintaining the same height the width decreases and we need to adjust measured width. // WARNING: This is a hack because we do not obey the EXACTLY MeasureSpec mode that we will get mostly. if (this.isReflectionEnabled) { this.setMeasuredDimension((int) (this.getMeasuredWidth() * this.originalScaledownFactor), this.getMeasuredHeight()); } } @SuppressLint("DrawAllocation") @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (changed) { int measuredWidth = this.getMeasuredWidth(); int measuredHeight = this.getMeasuredHeight(); if (this.wrappedViewBitmap == null || this.wrappedViewBitmap.getWidth() != measuredWidth || this.wrappedViewBitmap.getHeight() != measuredHeight) { this.wrappedViewBitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888); this.wrappedViewDrawingCanvas = new Canvas(this.wrappedViewBitmap); } View child = getChildAt(0); int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); int childLeft = (measuredWidth - childWidth) / 2; int childRight = measuredWidth - childLeft; child.layout(childLeft, 0, childRight, childHeight); } } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override protected void dispatchDraw(Canvas canvas) { View childView = getChildAt(0); if (childView != null) { // If on honeycomb or newer, cache the view. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { if (childView.isDirty()) { childView.draw(this.wrappedViewDrawingCanvas); if (this.isReflectionEnabled) { this.createReflectedImages(); } } } else { childView.draw(this.wrappedViewDrawingCanvas); } } canvas.drawBitmap(this.wrappedViewBitmap, (this.getWidth() - childView.getWidth()) / 2, 0, paint); } // ============================================================================= // Methods // ============================================================================= private void remeasureChildren() { View child = this.getChildAt(0); if (child != null) { // When reflection is enabled calculate proportional scale down factor. final int originalChildHeight = this.getMeasuredHeight(); this.originalScaledownFactor = this.isReflectionEnabled ? (originalChildHeight * (1 - this.imageReflectionRatio) - reflectionGap) / originalChildHeight : 1.0f; final int childHeight = (int) (this.originalScaledownFactor * originalChildHeight); final int childWidth = (int) (this.originalScaledownFactor * getMeasuredWidth()); int heightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST); int widthSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST); this.getChildAt(0).measure(widthSpec, heightSpec); } } /** * Creates the reflected images. * * @return true, if successful */ private void createReflectedImages() { final int width = this.wrappedViewBitmap.getWidth(); final int height = this.wrappedViewBitmap.getHeight(); final Matrix matrix = new Matrix(); matrix.postScale(1, -1); final int scaledDownHeight = (int) (height * originalScaledownFactor); final int invertedHeight = height - scaledDownHeight - reflectionGap; final int invertedBitmapSourceTop = scaledDownHeight - invertedHeight; final Bitmap invertedBitmap = Bitmap.createBitmap(this.wrappedViewBitmap, 0, invertedBitmapSourceTop, width, invertedHeight, matrix, true); this.wrappedViewDrawingCanvas.drawBitmap(invertedBitmap, 0, scaledDownHeight + reflectionGap, null); final Paint paint = new Paint(); final LinearGradient shader = new LinearGradient(0, height * imageReflectionRatio + reflectionGap, 0, height, 0x70ffffff, 0x00ffffff, Shader.TileMode.CLAMP); paint.setShader(shader); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); this.wrappedViewDrawingCanvas.drawRect(0, height * (1 - imageReflectionRatio), width, height, paint); } }