/** * The MIT License (MIT) * Copyright (c) 2012-2014 唐虞科技(TangyuSoft) Corporation * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.tangyu.component.view; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import java.util.LinkedList; import java.util.List; /** * <pre> * ------------------ * *****************| * *****************| * ***** MSGAREA ***| * *****************| * *****************| * **** ***| * **** HANDLE ***| * **** AREA ***| * **** ***| * ------------------ * </pre> * <p/> * Often use in guide</br> * <p/> * Cover with a black layer and multiple hollow. </br> * <p/> * if touch event position in handle area. The HollowListener will be handle it.</br> * <p/> * u can also set a message view to hollow to display the detail of hollow.</br> * * STEPS:<br> * <pre> * 1. create a hollow object. * 2. invoke {@link com.tangyu.component.view.TYHollowView.Hollow#calculateDelta(android.view.View, int)} to calculate the offset of hollow. * 3. create a message view, TextView ImageView or other, and set to hollow if need. * 4. if you set a message view. u can setting the position and gap params. * 5. add to hollow view. * </pre> * * SIMPLE CODE: <br> * <pre> * TYHollowView.Hollow hollow = new TYHollowView.Hollow(item); * TextView msgView = TYHollowView.Hollow.createSimpleTextView(this); * msgView.setText(sb.toString()); * Point delta = TYHollowView.Hollow.calculateDelta(item, mRootView.getId()); * hollow.setDelta(delta); * hollow.setPosition(position[i % position.length]); * hollow.setMsgView(msgView); * hollow.setGapBetweenMsgAndHollow((i + 1) * 20); * mRootView.addHollow(hollow); * </pre> * * @author bin */ public class TYHollowView extends FrameLayout { /** * log level should be greater than DEBUG. * if you want to show log info. please exec command as follow: * adb shell log.tag.TY DEBUG */ private static final String LOG_TAG = "TY"; public static Paint getDefPaint() { Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setTextSize(30f); paint.setStyle(Style.STROKE); paint.setColor(Color.WHITE); return paint; } protected Paint mPaint = getDefPaint(); protected List<Hollow> mHollows = new LinkedList<Hollow>(); protected TextView mVBlackLayout; protected HollowListener mListener; protected Hollow mFocusHollow; @SuppressLint("NewApi") public TYHollowView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); int sdkInt = android.os.Build.VERSION.SDK_INT; if (sdkInt >= 11) { setLayerType(LAYER_TYPE_SOFTWARE, null); } mVBlackLayout = new TextView(context, attrs, defStyle) { Rect rect = new Rect(); @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mHollows.size() > 0) { canvas.clipRect(0, 0, getWidth(), getHeight()); for (Hollow hollow : mHollows) { if (hollow.mHandler.getVisibility() != View.VISIBLE) { continue; } hollow.mHandler.getHitRect(rect); rect.offset(hollow.mDelta.x, hollow.mDelta.y); canvas.clipRect(rect, Region.Op.DIFFERENCE); } canvas.drawColor(0xAA000000); } } }; mVBlackLayout.setLayoutParams(new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); addView(mVBlackLayout); } public TYHollowView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public TYHollowView(Context context) { this(context, null, 0); } public void addHollow(Hollow hollow) { if (hollow != null && hollow.mVMsg != null) { mHollows.add(hollow); addView(hollow.mVMsg); requestLayout(); } } public void removeHollow(Hollow hollow) { if (null != mHollows && mHollows.size() > 0) { for (Hollow item : mHollows) { if (item.equals(hollow)) { mHollows.remove(item); // be careful if you wanna remove 'break' break; } } } } public final List<Hollow> getHollows() { return mHollows; } public void reset() { setVisibility(View.VISIBLE); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mFocusHollow = null; if (mHollows.size() > 0) { int x = (int) event.getX(), y = (int) event.getY(); Rect rect = new Rect(); for (Hollow hollow : mHollows) { hollow.mHandler.getHitRect(rect); if (rect.contains(x, y)) { mFocusHollow = hollow; onFocusHollowWhenTouchDown(hollow); break; } } } break; case MotionEvent.ACTION_UP: if (null != mListener) { boolean hasFocusHollow = null != mFocusHollow; mListener.onTappedListener(hasFocusHollow, hasFocusHollow ? mFocusHollow.mHandler : null); } break; } return true; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (mVBlackLayout.getVisibility() == View.VISIBLE) { mVBlackLayout.layout(left, top, right, bottom); } for (Hollow hollow : mHollows) { if (hollow.mVMsg != null && hollow.mVMsg.getVisibility() != View.GONE) { Rect rect = calculateLayoutOfHollowMsgView(hollow); onLayoutOfHollowMsgView(hollow, rect); hollow.mVMsg.layout(rect.left, rect.top, rect.right, rect.bottom); } } } private Rect calculateLayoutOfHollowMsgView(Hollow hollow) { Rect rect = new Rect(); Rect handleRect = new Rect(); hollow.mHandler.getHitRect(handleRect); handleRect.offset(hollow.mDelta.x, hollow.mDelta.y); int widthMeasureSpec, heightMeasureSpec; switch (hollow.mPos) { case Hollow.POS_LEFT_HOLLOW: rect.left = 0; rect.right = handleRect.left - hollow.mGapMsgAndHollow; rect.top = handleRect.top; widthMeasureSpec = MeasureSpec.makeMeasureSpec(rect.width(), MeasureSpec.AT_MOST); heightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST); hollow.mVMsg.measure(widthMeasureSpec, heightMeasureSpec); rect.left = rect.width() - hollow.mVMsg.getMeasuredWidth(); rect.bottom = rect.top + hollow.mVMsg.getMeasuredHeight(); break; case Hollow.POS_TOP_HOLLOW: rect.top = 0; rect.bottom = handleRect.top - hollow.mGapMsgAndHollow; widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST); heightMeasureSpec = MeasureSpec.makeMeasureSpec(rect.height(), MeasureSpec.AT_MOST); hollow.mVMsg.measure(widthMeasureSpec, heightMeasureSpec); rect.top = rect.bottom - hollow.mVMsg.getMeasuredHeight(); rect.left = handleRect.centerX() - (hollow.mVMsg.getMeasuredWidth() >> 1); rect.right = rect.left + hollow.mVMsg.getMeasuredWidth(); break; case Hollow.POS_RIGHT_HOLLOW: rect.left = handleRect.right + hollow.mGapMsgAndHollow; rect.right = getMeasuredWidth() - rect.left; rect.top = handleRect.top; widthMeasureSpec = MeasureSpec.makeMeasureSpec(rect.width(), MeasureSpec.AT_MOST); heightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST); hollow.mVMsg.measure(widthMeasureSpec, heightMeasureSpec); rect.right = rect.left + hollow.mVMsg.getMeasuredWidth(); rect.bottom = rect.top + hollow.mVMsg.getMeasuredHeight(); break; case Hollow.POS_BOTTOM_HOLLOW: rect.top = handleRect.bottom + hollow.mGapMsgAndHollow; rect.bottom = getMeasuredHeight(); widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST); heightMeasureSpec = MeasureSpec.makeMeasureSpec(rect.height(), MeasureSpec.AT_MOST); hollow.mVMsg.measure(widthMeasureSpec, heightMeasureSpec); rect.bottom = rect.top + hollow.mVMsg.getMeasuredHeight(); rect.left = handleRect.centerX() - (hollow.mVMsg.getMeasuredWidth() >> 1); rect.right = rect.left + hollow.mVMsg.getMeasuredWidth(); break; } log("w = " + hollow.mVMsg.getMeasuredWidth() + "|h = " + hollow.mVMsg.getMeasuredHeight()); log(rect.toString()); return rect; } @Override protected void onDraw(Canvas canvas) { // super.onDraw(canvas); if (getVisibility() == View.VISIBLE) { if (mVBlackLayout.getVisibility() == View.VISIBLE) { mVBlackLayout.draw(canvas); } // draw msg if (mHollows.size() > 0) { for (Hollow h : mHollows) { if (h != null && h.mVMsg.getVisibility() == View.VISIBLE) { h.mVMsg.draw(canvas); } } } } } /** * Layout the msg view of hollow.<br> * * If you wants to change the position of msg view. Just modify the parameter ‘rect’.<br> * * The rect is absolute coordinate in this view. * * @param hollow * @param rect */ protected void onLayoutOfHollowMsgView(Hollow hollow, Rect rect) { } /** * find out the hollow when user touch. * @param hollow */ protected void onFocusHollowWhenTouchDown(Hollow hollow) { } public void setOnHollowListener(HollowListener listener) { mListener = listener; } public interface HollowListener { /** * User was tapped view. * @param hasTappedHollow whether tapped in hollow or not. * @param view */ public void onTappedListener(boolean hasTappedHollow, View view); } public static class Hollow { public static final int POS_LEFT_HOLLOW = 0; public static final int POS_TOP_HOLLOW = 1; public static final int POS_RIGHT_HOLLOW = 2; public static final int POS_BOTTOM_HOLLOW = 3; protected View mHandler; protected View mVMsg; protected Point mDelta; protected int mPos; protected int mGapMsgAndHollow; public Hollow(View handler) { mHandler = handler; } public static TextView createSimpleTextView(Context ctx) { if (ctx == null) { throw new NullPointerException("ctx is null"); } TextView textView = new TextView(ctx); textView.setTextColor(Color.WHITE); textView.setLayoutParams(new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL)); return textView; } public static ImageView createSimpleImageView(Context ctx) { if (ctx == null) { throw new NullPointerException("ctx is null"); } ImageView imageView = new ImageView(ctx); imageView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); imageView.setScaleType(ImageView.ScaleType.CENTER); return imageView; } /** * To calculate the absolute coordinate between view with its super view. * @param handleView * @param dependViewId the super ID of handleView. * @return */ public static Point calculateDelta(View handleView, int dependViewId) { if (handleView == null) { throw new NullPointerException("handleView is null"); } ViewGroup parent; Point delta = new Point(); // log("DDDD " + delta.x + " | " + delta.y); parent = (ViewGroup) handleView.getParent(); delta.offset(parent.getLeft(), parent.getTop()); // log("DDDD " + delta.x + " | " + delta.y); while (parent != null && parent.getId() != dependViewId) { parent = (ViewGroup) parent.getParent(); if (parent != null) { delta.offset(parent.getLeft(), parent.getTop()); // log("DDDD " + delta.x + " | " + delta.y); } } return delta; } public void setDelta(Point delta) { if (delta == null) { throw new NullPointerException(); } mDelta = delta; } public void setPosition(int position) { if (position < 0 || position > 3) { throw new IllegalArgumentException(); } mPos = position; } public void setMsgView(View msgView) { mVMsg = msgView; } public void setGapBetweenMsgAndHollow(int gap) { mGapMsgAndHollow = gap; } } private static void log(String info) { if (Log.isLoggable(LOG_TAG, Log.DEBUG)) { if (!TextUtils.isEmpty(info)) Log.d(LOG_TAG, info); } } }