最近項目要實現一個圖片打標籤的需求,在這裏分享一個簡易版的打標籤:
1、點擊圖片任意位置跳轉到標籤列表頁,選擇後,標籤錨點到點擊位置。
2、點擊錨點反轉標籤。
3、拖拽標籤,限制在圖片區域內。
先上圖片方便理解:
實現的方案
1、用FramLayout:先加ImageView用於顯示圖片,再加標籤View顯示在圖片上層。
2、tagBean記錄 標籤錨點位置 與 圖片左上角距離的比例。
3、複雜的點擊事件處理。
源碼地址:https://github.com/shinecjj/PictureTag
PictureTagFrameLayout如下,其中最核心的方法onSizeChanged(int w, int h, int oldw, int oldh) 使用傳進來的圖片寬高比mImageWHRatio計算出圖片的mPhotoRectF,用來後面計算標籤相對於圖片的位置。
public class PictureTagFrameLayout extends FrameLayout{
private static final int CLICKRANGE = 5;
/**
* view
*/
private PictureTagView mTouchView;
private List<PictureTagView> mTagViewList;
/**
* data
*/
private List<ITagBean> mTagBeanList;
private ITagLayoutCallBack mTagLayoutCallBack;
private RectF mPhotoRectF; //圖片相對於framlayout的左上右下
private float mXUp, mYUp;
private float mStartX, mStartY;
private float mXDown, mYDown;
private float mTouchX, mTouchY;
private float dp27, dp25;
private float TAG_VIEW_HEIGHT;
private float TAG_VIEW_POINT_WIDTH;
private float mImageWHRatio;
public PictureTagFrameLayout(Context context) {
this(context, null);
}
public PictureTagFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context){
if(context == null){
return;
}
setClipChildren(false);
setClipToPadding(false);
dp27 = UIUtils.dip2Px(context, 27);
dp25 = UIUtils.dip2Px(context, 25);
TAG_VIEW_HEIGHT = dp25;
TAG_VIEW_POINT_WIDTH = dp27;
}
public void setTagLayoutCallBack(ITagLayoutCallBack tagLayoutCallBack){
mTagLayoutCallBack = tagLayoutCallBack;
}
public void notifyAddTagViewBasePhotoRect(RectF rect){
if(rect == null || rect.height() == 0 || rect.width() == 0){
return;
}
mPhotoRectF = rect;
if (mTagViewList != null && mTagViewList.size() > 0) {
for (PictureTagView pictureTagView : mTagViewList) {
if(pictureTagView != null) {
setTagViewLocation(pictureTagView);
addTagView(pictureTagView);
}
}
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w != oldw || h != oldh) {
int contentH = h - getPaddingBottom();
if(contentH > 0) {
float layoutWHRatio = 1.0f * w / contentH;
float left = 0, top = 0, right = w, bottom = contentH;
if (layoutWHRatio < mImageWHRatio) {
if(mImageWHRatio > 0) {
float imageHeight = w / mImageWHRatio;
top = (contentH - imageHeight) / 2f;
bottom = imageHeight + top;
}
} else if (layoutWHRatio > mImageWHRatio) {
float imageWidth = contentH * mImageWHRatio;
left = (w - imageWidth) / 2f;
right = left + imageWidth;
}
mPhotoRectF = new RectF(left, top, right, bottom);
if (mTagViewList != null && mTagViewList.size() > 0) {
for (PictureTagView pictureTagView : mTagViewList) {
if(pictureTagView != null) {
setTagViewLocation(pictureTagView);
addTagView(pictureTagView);
}
}
}
}
}
}
public void updateTagViews(List<ITagBean> list, float imageWHRatio){
if(list == null){
list = new ArrayList<>();
}
mTagBeanList = list;
mImageWHRatio = imageWHRatio;
/**
* 優化:初始化時先生成Views放在List中,後面滑到該頁面可直接使用
*/
if(list != null && !list.isEmpty()) {
if (mTagViewList == null) {
mTagViewList = new ArrayList<>();
}
for(int i = 0; i < list.size() && i < ITagBean.MAX_TAG_COUNT; i++) {
PictureTagView pictureTagView = createTagView(list.get(i));
if(pictureTagView != null) {
mTagViewList.add(pictureTagView);
}
}
}
}
/**
* 得到:左上角相對frameLayout的x,y座標
*/
private float[] getLtXY(PictureTagView pictureTagView){
if(pictureTagView == null || mPhotoRectF == null){
return new float[2];
}
ITagBean bean = pictureTagView.getTagBean();
if(bean == null){
return new float[2];
}
/**
* 計算錨點座標
*/
float x4Photo = bean.getSx() * mPhotoRectF.width(); //錨點相對圖片的x座標
float y4Photo = bean.getSy() * mPhotoRectF.height(); //錨點相對圖片的y座標
float x4Layout = mPhotoRectF.left + x4Photo; //錨點相對frameLayout的x座標
float y4Layout = mPhotoRectF.top + y4Photo; //錨點相對frameLayout的y座標
/**
* 錨點座標 轉換爲 左上角座標
*/
return pictureTagView.pointXY2LTXY(x4Layout, y4Layout);
}
private void moveView(float x, float y) {
if (mTouchView == null || mPhotoRectF == null) {
return;
}
/**
* 1、計算手指拖動距離
*/
float dragX = x - mTouchX;
float dragY = y - mTouchY;
/**
* 2、move事件的x,y出圖片區域的處理
*/
if(x >= mPhotoRectF.left && x <= mPhotoRectF.right) {
mTouchX = x;
}else if(x < mPhotoRectF.left){
mTouchX = mPhotoRectF.left;
}else if(x > mPhotoRectF.right){
mTouchX = mPhotoRectF.right;
}
if(y >= mPhotoRectF.top && y <= getBottom()) {
mTouchY = y;
}else if(y < mPhotoRectF.top){
mTouchY = mPhotoRectF.top;
}else if(y > getBottom()){
mTouchY = getBottom();
}
/**
* 3、計算新的左上角座標
*/
float[] oldLTXY = getLtXY(mTouchView); //舊的左上角座標
float[] newLTXY = {oldLTXY[0] + dragX, oldLTXY[1] + dragY}; //新的左上角座標
/**
* 4、限制tagView在圖片內移動
*/
handleNewLTXY(mTouchView, newLTXY);
float newLTX = newLTXY[0];
float newLTY = newLTXY[1];
/**
* 5、計算新鉚點座標
*/
float[] newPointXY = mTouchView.ltXY2PointXY(newLTX, newLTY);
/**
* 6、計算新鉚點座標比例,並更新數據
*/
if(newPointXY != null && newPointXY.length >= 2
&& mPhotoRectF != null && mPhotoRectF.width() > 0 && mPhotoRectF.height() > 0) {
float newXRatio = (newPointXY[0] - mPhotoRectF.left) / mPhotoRectF.width();
float newYRatio = (newPointXY[1] - mPhotoRectF.top) / mPhotoRectF.height();
ITagBean tagBean = mTouchView.getTagBean();
if (tagBean != null) {
tagBean.setSx(newXRatio);
tagBean.setSy(newYRatio);
}
}
/**
* 7、更新tag位置
*/
setTagViewLocation(mTouchView);
if(mTagLayoutCallBack != null){
mTagLayoutCallBack.onTagViewMoving();
}
}
private boolean deleteTagView(PictureTagView tagView){
if(tagView == null){
return false;
}
float[] ltXY = getLtXY(tagView);
if(ltXY[1] > getBottom() - getPaddingBottom()){
if(mTagViewList != null){
mTagViewList.remove(tagView);
}
if(mTagBeanList != null){
mTagBeanList.remove(tagView.getTagBean());
}
PictureTagFrameLayout.this.removeView(tagView);
tagView = null;
return true;
}
return false;
}
/**
* 根據tagView不超出mPhotoRectF邊界爲原則,處理變換後的左上角的座標
* @param tagView
* @param newLTXY
*/
private void handleNewLTXY(PictureTagView tagView, float[] newLTXY){
if(tagView == null || newLTXY == null || newLTXY.length < 2 || mPhotoRectF == null){
return;
}
/**
* 1、計算tagView的l,t,r,b, 得到
*/
float newL = newLTXY[0]; //新的相對於framlayout的left
float newT = newLTXY[1]; //新的相對於framlayout的top
float newR = newL + tagView.getMeasuredWidth(); //新的相對於framlayout的right
float newB = newT + tagView.getMeasuredHeight(); //新的相對於framlayout的bottom
/**
* 2、判斷是否在photo的RectF內
* getBottom() 是因爲 需要可以拖到刪除區域
*/
if(newL < mPhotoRectF.left){
newLTXY[0] = mPhotoRectF.left;
}else if(newR > mPhotoRectF.right){
newLTXY[0] = mPhotoRectF.right - tagView.getMeasuredWidth() ;
}
if(newT < mPhotoRectF.top){
newLTXY[1] = mPhotoRectF.top;
}else if(newB > getBottom()){
newLTXY[1] = getBottom() - tagView.getMeasuredHeight();
}
}
private boolean checkTagBean(ITagBean bean){
if(bean == null || TextUtils.isEmpty(bean.getTagName())){
return false;
}
if(bean.getSx() <= 0 || bean.getSx() >= 1){
return false;
}
if(bean.getSy() <= 0 || bean.getSy() >= 1){
return false;
}
return true;
}
public void addItem(ITagBean bean) {
if(!checkTagBean(bean)){
return;
}
/**
* 1、生成tagView
*/
PictureTagView tagView = createTagView(bean);
/**
* 2、設置座標
*/
setTagViewLocation(tagView);
if (mTagViewList == null) {
mTagViewList = new ArrayList<>();
}else if(mTagViewList.size() >= ITagBean.MAX_TAG_COUNT){
Toast.makeText(getContext(), "最多可添加15個標籤", Toast.LENGTH_LONG);
return;
}
mTagViewList.add(tagView);
if(mTagBeanList == null) {
mTagBeanList = new ArrayList<>();
}else if(mTagBeanList.size() >= ITagBean.MAX_TAG_COUNT){
Toast.makeText(getContext(), "最多可添加15個標籤", Toast.LENGTH_LONG);
return;
}
mTagBeanList.add(bean);
addTagView(tagView);
}
private boolean addTagView(PictureTagView tagView){
if(tagView == null){
return false;
}
if(!tagView.isHasByAdded()){
addView(tagView);
tagView.setHasByAdded(true);
return true;
}
return false;
}
private PictureTagView createTagView(ITagBean bean){
if(bean == null || bean.isHasAdded()){
return null;
}
PictureTagView tagView = null;
if(ARROW_RIGHT == bean.getArrow() || ARROW_LEFT == bean.getArrow()) {
tagView = new PictureTagView(getContext(), bean);
}else {
if (bean.getSx() > 0.5) {//Right是指 點在右邊
bean.setArrow(ARROW_LEFT);
tagView = new PictureTagView(getContext(), bean);
} else {
bean.setArrow(ARROW_RIGHT);
tagView = new PictureTagView(getContext(), bean);
}
}
bean.setHasAdded(true);
return tagView;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
mXDown = ev.getX();
mYDown = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mTouchView = null;
/**
* 1、點擊到tag上後,攔截事件
* 2、禁止父view攔截事件(防止父view -- viewPage 攔截事件進行橫劃操作)
*/
if (hasView(mXDown, mYDown)) {
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
return true;
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartX = event.getX();
mStartY = event.getY();
//手指拖動前座標
mTouchX = mStartX;
mTouchY = mStartY;
break;
case MotionEvent.ACTION_MOVE:
/**
* 隨手指滑動
*/
if(!isSingleClick(mStartX, event.getX(), mStartY, event.getY())) {
moveView(event.getX(), event.getY());
}
break;
case MotionEvent.ACTION_UP:
mXUp = (int) event.getX();
mYUp = (int) event.getY();
if(mTagLayoutCallBack != null){
mTagLayoutCallBack.onTagViewStopMoving();
}
/**
* 單擊事件,並且單擊到鉚點上,則轉換方向
*/
if (mTouchView != null && isSingleClick(mStartX, mXUp, mStartY, mYUp)) {
if (isOnViewPoint(mXUp, mYUp)) {
changeTagViewDirection(mTouchView);
}
}
/**
* 滑動到底部刪除區域則進行刪除
*/
if(!deleteTagView(mTouchView)) {
/**
* 滑動超出photo下面區域,up時重置其位置
*/
resetTagViewLocationWhenUp(mTouchView);
}
break;
}
return true;
}
private void changeTagViewDirection(PictureTagView tagView){
if(tagView == null){
return;
}
/**
* 1、轉換方向
*/
tagView.directionChange();
/**
* 2、左上角相對frameLayout的x,y座標
*/
float[] newLTXY = getLtXY(tagView);
/**
* 3、根據不超出邊界重新賦值newLTXY
*/
handleNewLTXY(tagView, newLTXY);
/**
* 4、根據重新賦值的newLTXY重新set鉚點座標比例
*/
float[] pointXY = tagView.ltXY2PointXY(newLTXY[0], newLTXY[1]);
ITagBean tagBean = tagView.getTagBean();
if(tagBean != null && pointXY != null && pointXY.length >= 2
&& mPhotoRectF != null && mPhotoRectF.width() > 0 && mPhotoRectF.height() > 0){
float newXRatio = (pointXY[0] - mPhotoRectF.left) / mPhotoRectF.width();
float newYRatio = (pointXY[1] - mPhotoRectF.top) / mPhotoRectF.height();
tagBean.setSx(newXRatio);
tagBean.setSy(newYRatio);
}
/**
* 5、設置tagView的位置
*/
setTagViewLocation(tagView);
}
private void resetTagViewLocationWhenUp(PictureTagView tagView){
if(tagView == null || mPhotoRectF == null){
return;
}
float[] newLTXY = getLtXY(tagView);
/**
* 1、計算tagView的l,t,r,b, 得到
*/
float newT = newLTXY[1]; //新的相對於framlayout的top
float newB = newT + tagView.getMeasuredHeight(); //新的相對於framlayout的bottom
/**
* 2、判斷是否超過了photo的RectF的底部
*/
if(newB > mPhotoRectF.bottom) {
newLTXY[1] = mPhotoRectF.bottom - tagView.getMeasuredHeight();
/**
* 3、根據重新賦值的newLTXY重新set鉚點座標比例
*/
float[] pointXY = tagView.ltXY2PointXY(newLTXY[0], newLTXY[1]);
ITagBean tagBean = tagView.getTagBean();
if (tagBean != null && pointXY != null && pointXY.length >= 2
&& mPhotoRectF != null && mPhotoRectF.width() > 0 && mPhotoRectF.height() > 0) {
float newXRatio = (pointXY[0] - mPhotoRectF.left) / mPhotoRectF.width();
float newYRatio = (pointXY[1] - mPhotoRectF.top) / mPhotoRectF.height();
tagBean.setSx(newXRatio);
tagBean.setSy(newYRatio);
}
/**
* 4、重新設置位置
*/
setTagViewLocation(tagView);
}
}
private void setTagViewLocation(PictureTagView tagView){
if(tagView == null){
return;
}
/**
* 得到:左上角相對frameLayout的x,y座標
*/
float[] ltXY = getLtXY(tagView);
if(ltXY != null) {
tagView.setX(ltXY[0]);
tagView.setY(ltXY[1]);
}
}
/**
* 循環獲取子view,判斷xy是否在子view上,即判斷是否按住了子view
*/
private boolean hasView(float x, float y) {
for (int index = 0; index < this.getChildCount(); index++) {
View view = this.getChildAt(index);
if (!(view instanceof PictureTagView)) {
continue;
}
float left = view.getX();
float top = view.getY();
float right = view.getWidth() + left;
float bottom = view.getHeight() + top;
RectF rectf = new RectF(left, top, right, bottom);
boolean contains = rectf.contains(x, y);
if (contains) {
mTouchView = (PictureTagView) view;
mTouchView.bringToFront();
return true;
}
}
mTouchView = null;
return false;
}
/**
* 循環獲取子view,判斷xy是否在tagView的鉚點區域上
*/
private boolean isOnViewPoint(float x, float y) {
for (int index = 0; index < this.getChildCount(); index++) {
View view = this.getChildAt(index);
if (!(view instanceof PictureTagView)) {
continue;
}
/**
* 1、計算tagView左上角的座標(相對於此framLayout)
*/
float ltX = view.getX(); //tagView左上角的x座標
float ltY = view.getY(); //tagView左上角的y座標
/**
* 2、計算鉚點區域的RectF
*/
RectF rectF = new RectF();
if(((PictureTagView) view).isRightArrow()){
float pointLeft = ltX;
float pointTop = ltY;
float pointRight = pointLeft + TAG_VIEW_POINT_WIDTH;
float pointBottom = pointTop + TAG_VIEW_HEIGHT;
rectF.set(pointLeft, pointTop, pointRight, pointBottom);
}else if(((PictureTagView) view).isLeftArrow()){
float pointLeft = ltX + view.getMeasuredWidth() - TAG_VIEW_POINT_WIDTH;
float pointTop = ltY;
float pointRight = pointLeft + TAG_VIEW_POINT_WIDTH;
float pointBottom = pointTop + TAG_VIEW_HEIGHT;
rectF.set(pointLeft, pointTop, pointRight, pointBottom);
}
/**
* 3、判斷點擊位置是否在rectF中
*/
boolean contains = rectF.contains(x, y);
if (contains) {
mTouchView = (PictureTagView) view;
mTouchView.bringToFront();
return true;
}
}
mTouchView = null;
return false;
}
private boolean isSingleClick(float startX, float endX, float startY, float endY){
if(Math.abs(endX - startX) < CLICKRANGE && Math.abs(endY - startY) < CLICKRANGE){
return true;
}else {
return false;
}
}
public interface ITagLayoutCallBack {
void onSingleClick(float x, float y);
void onTagViewMoving();
void onTagViewStopMoving();
}
}