/* * Copyright (C) 2015 The App Business. * * 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.nabilhachicha.kc.view; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ComposeShader; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import com.nabilhachicha.kc.R; import com.nabilhachicha.kc.model.POI; import com.nabilhachicha.kc.utils.ViewUtils; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; /** * Created by Nabil Hachicha on 14/10/14. */ public class TiltedCardView extends View { private Path mPathTop; private Path mPathBottom; private Paint mCardTitlePaint; private TextPaint mCardBodyPaint; private Target mTargetTop = new Target() { @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { Shader shaderTop = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); Shader gradient = new LinearGradient(0, bitmap.getHeight(), 0, 0, new int[]{ Color.BLACK, Color.TRANSPARENT}, null, Shader.TileMode.CLAMP); mPaintTop.setShader( new ComposeShader(shaderTop, gradient, PorterDuff.Mode.SRC_OVER)); invalidate(); } @Override public void onBitmapFailed(Drawable errorDrawable) { // show place holder } @Override public void onPrepareLoad(Drawable placeHolderDrawable) { // show place holder } }; private Target mTargetBottom = new Target() { @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { Shader shaderBottom = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); Shader gradient = new LinearGradient(0, bitmap.getHeight(), 0, 0, new int[]{ Color.BLACK, Color.TRANSPARENT}, null, Shader.TileMode.CLAMP); mPaintBottom.setShader( new ComposeShader(shaderBottom, gradient, PorterDuff.Mode.SRC_OVER)); invalidate(); } @Override public void onBitmapFailed(Drawable errorDrawable) { // show place holder } @Override public void onPrepareLoad(Drawable placeHolderDrawable) { // show place holder } }; public static interface OnPoiClickedListener { public void onPoiClick(POI poi); } OnPoiClickedListener mListener; private GestureDetector mDetector; private POI mTopPoi; private POI mBottomPoi; private Paint mPaintSeparator; private Paint mPaintBottom; private Paint mPaintTop; private Region mRegionTop; private static int WIDTH; private static int HEIGHT; private final static int TILT_ANGLE = 7; //TODO integer degree (load from style) private static int SEPARATOR_WIDTH = 10;//TODO (in pixel) uses style with DP private static int SEPARATOR_COLOR; private static int CARD_DETAILS_WIDTH; private static int CARD_DETAILS_HEIGHT; private final static int CARD_DETAILS_MARGIN = 40;//TODO (in pixel) uses style with DP private static int SHIFT; private Rect mCardTopRect; private Rect mCardBottomRect; private StaticLayout mStaticLytTopBodyTxt; private StaticLayout mStaticLytBottomBodyTxt; private Rect mTitleRect; private int titleTextSize, descriptionTextSize; private int mColorTitle, mColorDescription; public TiltedCardView(Context context, OnPoiClickedListener listener) { super(context); mListener = listener; // Reading style TypedArray a = context.obtainStyledAttributes(null, R.styleable.TiltedView, 0, 0); SEPARATOR_WIDTH = (int) a.getDimension(R.styleable.TiltedView_tv_separatorSize, 10.0f); SEPARATOR_COLOR = a.getColor(R.styleable.TiltedView_tv_separatorColor, Color.WHITE); // textSize is already expressed in pixels titleTextSize = a.getDimensionPixelSize(R.styleable.TiltedView_tv_titleTextSize, 30); descriptionTextSize = a.getDimensionPixelSize(R.styleable.TiltedView_tv_descriptionTextSize, 40); mColorTitle = a.getColor(R.styleable.TiltedView_tv_titleColor, Color.WHITE); mColorDescription = a.getColor(R.styleable.TiltedView_tv_descriptionColor, Color.WHITE); //TODO read other properties (text size colors etc). a.recycle(); } public TiltedCardView(Context context, POI top, POI bottom) { super(context); mTopPoi = top; mBottomPoi = bottom; // Reading style TypedArray a = context.obtainStyledAttributes(null, R.styleable.TiltedView, 0, 0); SEPARATOR_WIDTH = (int) a.getDimension(R.styleable.TiltedView_tv_separatorSize, 10.0f); SEPARATOR_COLOR = a.getColor(R.styleable.TiltedView_tv_separatorColor, Color.WHITE); // textSize is already expressed in pixels titleTextSize = a.getDimensionPixelSize(R.styleable.TiltedView_tv_titleTextSize, 30); descriptionTextSize = a.getDimensionPixelSize(R.styleable.TiltedView_tv_descriptionTextSize, 40); mColorTitle = a.getColor(R.styleable.TiltedView_tv_titleColor, Color.WHITE); mColorDescription = a.getColor(R.styleable.TiltedView_tv_descriptionColor, Color.WHITE); //TODO read other properties (text size colors etc). a.recycle(); init(); } public void bindTo(POI top, POI bottom, Picasso picasso) { mTopPoi = top; mBottomPoi = bottom; init(); requestLayout(); } public void setListener(OnPoiClickedListener listener) { mListener = listener; } private void init() { // Geometry & sizes WIDTH = getContext().getResources().getDisplayMetrics().widthPixels;//TODO maybe too big for tablet or landscape (use correct onMeasure) HEIGHT = WIDTH; CARD_DETAILS_WIDTH = WIDTH / 2; CARD_DETAILS_HEIGHT = (int) (WIDTH / 4 * 0.7); SHIFT = (int) (Math.tan(Math.toRadians(TILT_ANGLE)) * WIDTH); mCardTopRect = new Rect(); mCardBottomRect = new Rect(); // Placeholder image Bitmap placeholderBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.placeholder); Shader shaderPlaceholder = new BitmapShader(placeholderBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); // Various paints mPaintSeparator = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintSeparator.setColor(SEPARATOR_COLOR); mPaintSeparator.setStrokeWidth(SEPARATOR_WIDTH); mPaintSeparator.setStyle(Paint.Style.FILL); mPaintTop = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintTop.setShader(shaderPlaceholder); mPaintBottom = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintBottom.setShader(shaderPlaceholder); mCardTitlePaint = new Paint(); mCardTitlePaint.setTypeface(ViewUtils.getTypeface(getContext(), "apercu_bold")); mCardTitlePaint.setColor(mColorTitle); mCardTitlePaint.setTextSize(titleTextSize); mCardBodyPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mCardTitlePaint.setTypeface(ViewUtils.getTypeface(getContext(), "apercu_regular")); mCardBodyPaint.setTextSize(descriptionTextSize); mCardBodyPaint.setColor(mColorDescription); //Detect a click on the top/bottom component mDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { return true; } @Override public boolean onSingleTapUp(MotionEvent e) { return super.onSingleTapUp(e); } @Override public boolean onSingleTapConfirmed(MotionEvent event) { Point point = new Point(); point.x = (int) event.getX(); point.y = (int) event.getY(); if (mRegionTop.contains(point.x, point.y)) { mListener.onPoiClick(mTopPoi); } else { mListener.onPoiClick(mBottomPoi); } return super.onSingleTapConfirmed(event); } }); // Define the boundaries of the bottom part. mPathBottom = new Path(); mPathBottom.moveTo(0, 0); mPathBottom.lineTo(WIDTH, 0); mPathBottom.lineTo(WIDTH, HEIGHT / 2); mPathBottom.lineTo(0, HEIGHT / 2); mPathBottom.close(); // Define the boundaries of the top part. mPathTop = new Path(); mPathTop.moveTo(0, 0); mPathTop.lineTo(WIDTH, 0); mPathTop.lineTo(WIDTH, HEIGHT / 2 - SHIFT); mPathTop.lineTo(0, HEIGHT / 2); mPathTop.close(); // Define a region to help us detect whether the click is for the top or bottom part RectF rectF = new RectF(); mPathTop.computeBounds(rectF, true); mRegionTop = new Region(); mRegionTop.setPath(mPathTop, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom)); // misc mStaticLytTopBodyTxt = new StaticLayout(mTopPoi.getEllipsizedDescription(), mCardBodyPaint, CARD_DETAILS_MARGIN + CARD_DETAILS_WIDTH, Layout.Alignment.ALIGN_NORMAL, 1, 1, false); if (null != mBottomPoi) { mStaticLytBottomBodyTxt = new StaticLayout(mBottomPoi.getEllipsizedDescription(), mCardBodyPaint, CARD_DETAILS_WIDTH - CARD_DETAILS_MARGIN, Layout.Alignment.ALIGN_NORMAL, 1, 1, false); } mTitleRect = new Rect(); // Load the images Picasso.with(getContext()).load(mTopPoi.getImgUrl()).resize(WIDTH, (WIDTH / 2) + SHIFT).centerCrop().into(mTargetTop); if (null != mBottomPoi) { Picasso.with(getContext()).load(mBottomPoi.getImgUrl()).resize(WIDTH, (WIDTH / 2) + SHIFT).centerCrop().into(mTargetBottom); } } @Override public boolean onTouchEvent(MotionEvent event) { return mDetector.onTouchEvent(event); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO write a decent onMeasure that takes into account orientation & tablet setMeasuredDimension(WIDTH, HEIGHT - SHIFT); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //TODO add case where only top is available drawTopAndBottom(canvas); drawSeparator(canvas); drawCardDetailTop(canvas); if (null != mBottomPoi) drawCardDetailBottom(canvas); } private void drawTopAndBottom(final Canvas canvas) { canvas.save(); canvas.translate(0, HEIGHT / 2 - SHIFT); canvas.drawPath(mPathBottom, mPaintBottom); canvas.restore(); canvas.drawPath(mPathTop, mPaintTop); } private void drawSeparator(final Canvas canvas) { canvas.drawLine(0, HEIGHT / 2, WIDTH, HEIGHT / 2 - SHIFT, mPaintSeparator); } private void drawCardDetailTop(final Canvas canvas) { // Bounds of this card mCardTopRect.set(CARD_DETAILS_MARGIN, HEIGHT / 4 + (int) (SHIFT * 0.5), CARD_DETAILS_MARGIN + CARD_DETAILS_WIDTH, HEIGHT / 4 + (int) (SHIFT * 0.5) + CARD_DETAILS_HEIGHT); // Draw title text mCardTitlePaint.getTextBounds(mTopPoi.getName(), 0, mTopPoi.getName().length(), mTitleRect); canvas.drawText(mTopPoi.getName(), mCardTopRect.left, mCardTopRect.top, mCardTitlePaint); // Draw Body text canvas.save(); canvas.translate(mCardTopRect.left, mCardTopRect.top + mTitleRect.height()); mStaticLytTopBodyTxt.draw(canvas); canvas.restore(); } private void drawCardDetailBottom(Canvas canvas) { // Bounds of this card mCardBottomRect.set(getMeasuredWidth() - CARD_DETAILS_WIDTH - CARD_DETAILS_MARGIN, (int) (HEIGHT * 0.75) - (int) (SHIFT * 0.5), getMeasuredWidth() - CARD_DETAILS_MARGIN, getMeasuredHeight() - CARD_DETAILS_MARGIN); // Draw title text mCardTitlePaint.getTextBounds(mBottomPoi.getName(), 0, mBottomPoi.getName().length(), mTitleRect); canvas.drawText(mBottomPoi.getName(), mCardBottomRect.left, mCardBottomRect.top, mCardTitlePaint); // Draw Body text canvas.save(); canvas.translate(mCardBottomRect.left, mCardBottomRect.top + mTitleRect.height()); mStaticLytBottomBodyTxt.draw(canvas); canvas.restore(); } }