Android图片打标签

2022-06-25 10:52:54 浏览数 (3)

代码语言:javascript复制
  最近项目要实现一个图片打标签的需求,在这里分享一个简易版的打标签:
     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,用来后面计算标签相对于图片的位置。

代码语言:javascript复制
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();
    }
}

0 人点赞