图片裁剪,仿淘宝搜图类似裁剪

相框移动,四个角可以拖动,随便剪接

自定义类

package com.diction.app.android._av7.view.crop_iamge;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import com.diction.app.android._av7._view.utils.PrintUtilsJava;

public class CropImageView extends View {
    // 在touch重要用到的点,
    private float mX_1 = 0;
    private float mY_1 = 0;
    // 触摸事件判断
    private final int STATUS_SINGLE = 1;
    private final int STATUS_MULTI_START = 2;
    private final int STATUS_MULTI_TOUCHING = 3;
    // 当前状态
    private int mStatus = STATUS_SINGLE;
    // 默认裁剪的宽高
    private int cropWidth;
    private int cropHeight;
    // 浮层Drawable的四个点
    private final int EDGE_LT = 1;
    private final int EDGE_RT = 2;
    private final int EDGE_LB = 3;
    private final int EDGE_RB = 4;
    private final int EDGE_MOVE_IN = 5;
    private final int EDGE_MOVE_OUT = 6;
    private final int EDGE_NONE = 7;

    public int currentEdge = EDGE_NONE;

    protected float oriRationWH = 0;

    protected Drawable mDrawable;
    protected FloatDrawable mFloatDrawable;

    protected Rect mDrawableSrc = new Rect();// 图片Rect变换时的Rect
    protected Rect mDrawableDst = new Rect();// 图片Rect
    protected Rect mDrawableFloat = new Rect();// 浮层的Rect
    protected boolean isFirst = true;
    private boolean isTouchInSquare = true;

    protected Context mContext;
    private Bitmap mBitmap;

    private boolean  mcanMove = true;

    public void setCanMove(boolean canMove) {
        this.mcanMove = canMove;
        PrintUtilsJava.pringtLog("setCanMove---->"+ canMove);
    }

    public CropImageView(Context context) {
        super(context);
        init(context);
    }

    public CropImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public CropImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    @SuppressLint("NewApi")
    private void init(Context context) {
        this.mContext = context;
        try {
            if (android.os.Build.VERSION.SDK_INT >= 11) {
                this.setLayerType(LAYER_TYPE_SOFTWARE, null);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        mFloatDrawable = new FloatDrawable(context);
    }

    public void setDrawable(Bitmap bitmap, int cropWidth, int cropHeight) {
        mBitmap = bitmap;
        this.mDrawable = new BitmapDrawable(bitmap);
        this.cropWidth = cropWidth;
        this.cropHeight = cropHeight;
        this.isFirst = true;
        invalidate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (mBitmap != null) {
            if ((mBitmap.getHeight() > heightSize) && (mBitmap.getHeight() > mBitmap.getWidth())) {
                widthSize = heightSize * mBitmap.getWidth() / mBitmap.getHeight();
            } else if ((mBitmap.getWidth() > widthSize) && (mBitmap.getWidth() > mBitmap.getHeight())) {
                heightSize = widthSize * mBitmap.getHeight() / mBitmap.getWidth();
            } else {
                heightSize = mBitmap.getHeight();
                widthSize = mBitmap.getWidth();
            }
        }
        setMeasuredDimension(widthSize, heightSize);
    }
    private boolean hasMover = false;
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mcanMove){
            return  false;
        }
        if (event.getPointerCount() > 1) {
            if (mStatus == STATUS_SINGLE) {
                mStatus = STATUS_MULTI_START;
            } else if (mStatus == STATUS_MULTI_START) {
                mStatus = STATUS_MULTI_TOUCHING;
            }
        } else {
            if (mStatus == STATUS_MULTI_START
                    || mStatus == STATUS_MULTI_TOUCHING) {
                mX_1 = event.getX();
                mY_1 = event.getY();
            }

            mStatus = STATUS_SINGLE;
        }

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mX_1 = event.getX();
                mY_1 = event.getY();
                currentEdge = getTouch((int) mX_1, (int) mY_1);
                break;

            case MotionEvent.ACTION_UP:
                if (hasMover ){
                    if(moverListner != null){
                        moverListner.onMoverListner();
                    }
                }

                hasMover = false;
                break;

            case MotionEvent.ACTION_POINTER_UP:
                currentEdge = EDGE_NONE;
                break;

            case MotionEvent.ACTION_MOVE:
                if (mStatus == STATUS_MULTI_TOUCHING) {

                } else if (mStatus == STATUS_SINGLE) {
                    int dx = (int) (event.getX() - mX_1);
                    int dy = (int) (event.getY() - mY_1);

                    mX_1 = event.getX();
                    mY_1 = event.getY();
                    // TODO 如果手指坐标超出view范围,则返回。
                    // 理论上应该写在这里,但会产生一个移动四个角过快的时候不容易移到到边界的现象
//                    if (mX_1 > getWidth() || mX_1 < 0 || mY_1 > getHeight() || mY_1 < 0) {
//                        break;
//                    }
                    // 根據得到的那一个角,并且变换Rect
                    if (!(dx == 0 && dy == 0)) {
                        switch (currentEdge) {
                            case EDGE_LT:
                                hasMover = true;
                                int left = mDrawableFloat.left +dx;
                                int right = mDrawableFloat.right;
                                int top = mDrawableFloat.top+dy;
                                int bottom = mDrawableFloat.bottom;

                                Log.e("mover_","left -->"+left +"  right = "+ right +   "  top = "+ top+"  bottom= "+ bottom);

                                if (right -left<90){
                                    right= left+90;
                                }

                                if (bottom- top<90){
                                    bottom = top+90;
                                }

                                mDrawableFloat.set(left,top,right,bottom);

                               /* mDrawableFloat.set(mDrawableFloat.left + dx,
                                        mDrawableFloat.top + dy,
                                        mDrawableFloat.right,
                                        mDrawableFloat.bottom);*/
                                break;

                            case EDGE_RT:
                                hasMover = true;
                                int leftR = mDrawableFloat.left ;
                                int rightR = mDrawableFloat.right+dx;
                                int topR = mDrawableFloat.top+dy;
                                int bottomR = mDrawableFloat.bottom;

                                if (rightR -leftR<90){
                                    rightR= leftR+90;
                                }

                                if (bottomR- topR<90){
                                    bottomR = topR+90;
                                }

                                mDrawableFloat.set(leftR,topR,rightR,bottomR);

                   /*             mDrawableFloat.set(mDrawableFloat.left,
                                        mDrawableFloat.top + dy,
                                        mDrawableFloat.right + dx,
                                        mDrawableFloat.bottom);*/
                                break;

                            case EDGE_LB:
                                hasMover = true;
                                int leftLB = mDrawableFloat.left +dx;
                                int rightLB = mDrawableFloat.right;
                                int topLB = mDrawableFloat.top;
                                int bottomLB = mDrawableFloat.bottom +dy;


                                if (rightLB -leftLB<90){
                                    rightLB= leftLB+90;
                                }

                                if (bottomLB- topLB<90){
                                    bottomLB = topLB+90;
                                }

                                mDrawableFloat.set(leftLB,topLB,rightLB,bottomLB);

                             /*   mDrawableFloat.set(mDrawableFloat.left + dx,
                                        mDrawableFloat.top,
                                        mDrawableFloat.right,
                                        mDrawableFloat.bottom + dy);*/
                                break;

                            case EDGE_RB:
                                hasMover = true;
                                int leftRB = mDrawableFloat.left ;
                                int rightRB = mDrawableFloat.right+dx;
                                int topRB = mDrawableFloat.top;
                                int bottomRB = mDrawableFloat.bottom +dy;
                                if (rightRB -leftRB<90){
                                    rightRB= leftRB+90;
                                }

                                if (bottomRB- topRB<90){
                                    bottomRB = topRB+90;
                                }

                                mDrawableFloat.set(leftRB,topRB,rightRB,bottomRB);

                    /*            mDrawableFloat.set(mDrawableFloat.left,
                                        mDrawableFloat.top,
                                        mDrawableFloat.right + dx,
                                        mDrawableFloat.bottom + dy);*/
                                break;

                            case EDGE_MOVE_IN:
                                hasMover = true;
                                // 因为手指一直在移动,应该实时判断是否超出裁剪框(手指移动到图片范围外)
                                isTouchInSquare = mDrawableFloat.contains((int) event.getX(),
                                        (int) event.getY());
                                if (isTouchInSquare) {
                                    mDrawableFloat.offset(dx, dy);
                                }
                                break;

                            case EDGE_MOVE_OUT:
                                break;
                        }
                        mDrawableFloat.sort();
                        invalidate();
                    }
                }
                break;
        }
/*        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mX_1 = event.getX();
                mY_1 = event.getY();
                currentEdge = getTouch((int) mX_1, (int) mY_1);
                break;

            case MotionEvent.ACTION_UP:
                break;

            case MotionEvent.ACTION_POINTER_UP:
                currentEdge = EDGE_NONE;
                break;

            case MotionEvent.ACTION_MOVE:
                if (mStatus == STATUS_MULTI_TOUCHING) {

                } else if (mStatus == STATUS_SINGLE) {
                    int dx = (int) (event.getX() - mX_1);
                    int dy = (int) (event.getY() - mY_1);

                    mX_1 = event.getX();
                    mY_1 = event.getY();
                    // TODO 如果手指坐标超出view范围,则返回。
                    // 理论上应该写在这里,但会产生一个移动四个角过快的时候不容易移到到边界的现象
//                    if (mX_1 > getWidth() || mX_1 < 0 || mY_1 > getHeight() || mY_1 < 0) {
//                        break;
//                    }
                    // 根據得到的那一个角,并且变换Rect
                    if (!(dx == 0 && dy == 0)) {
                        switch (currentEdge) {
                            case EDGE_LT:
                                mDrawableFloat.set(mDrawableFloat.left + dx,
                                        mDrawableFloat.top + dy,
                                        mDrawableFloat.right,
                                        mDrawableFloat.bottom);
                                break;

                            case EDGE_RT:
                                mDrawableFloat.set(mDrawableFloat.left,
                                        mDrawableFloat.top + dy,
                                        mDrawableFloat.right + dx,
                                        mDrawableFloat.bottom);
                                break;

                            case EDGE_LB:
                                mDrawableFloat.set(mDrawableFloat.left + dx,
                                        mDrawableFloat.top,
                                        mDrawableFloat.right,
                                        mDrawableFloat.bottom + dy);
                                break;

                            case EDGE_RB:
                                mDrawableFloat.set(mDrawableFloat.left,
                                        mDrawableFloat.top,
                                        mDrawableFloat.right + dx,
                                        mDrawableFloat.bottom + dy);
                                break;

                            case EDGE_MOVE_IN:
                                // 因为手指一直在移动,应该实时判断是否超出裁剪框(手指移动到图片范围外)
                                isTouchInSquare = mDrawableFloat.contains((int) event.getX(),
                                        (int) event.getY());
                                if (isTouchInSquare) {
                                    mDrawableFloat.offset(dx, dy);
                                }
                                break;

                            case EDGE_MOVE_OUT:
                                break;
                        }
                        mDrawableFloat.sort();
                        invalidate();
                    }
                }
                break;
        }*/
        return true;
    }

    // 根据初触摸点判断是触摸的Rect哪一个角
    public int getTouch(int eventX, int eventY) {
        Rect mFloatDrawableRect = mFloatDrawable.getBounds();
        int mFloatDrawableWidth = mFloatDrawable.getBorderWidth();
        int mFloatDrawableHeight = mFloatDrawable.getBorderHeight();
        if (mFloatDrawableRect.left <= eventX
                && eventX < (mFloatDrawableRect.left + mFloatDrawableWidth)
                && mFloatDrawableRect.top <= eventY
                && eventY < (mFloatDrawableRect.top + mFloatDrawableHeight)) {
            return EDGE_LT;
        } else if ((mFloatDrawableRect.right - mFloatDrawableWidth) <= eventX
                && eventX < mFloatDrawableRect.right
                && mFloatDrawableRect.top <= eventY
                && eventY < (mFloatDrawableRect.top + mFloatDrawableHeight)) {
            return EDGE_RT;
        } else if (mFloatDrawableRect.left <= eventX
                && eventX < (mFloatDrawableRect.left + mFloatDrawableWidth)
                && (mFloatDrawableRect.bottom - mFloatDrawableHeight) <= eventY
                && eventY < mFloatDrawableRect.bottom) {
            return EDGE_LB;
        } else if ((mFloatDrawableRect.right - mFloatDrawableWidth) <= eventX
                && eventX < mFloatDrawableRect.right
                && (mFloatDrawableRect.bottom - mFloatDrawableHeight) <= eventY
                && eventY < mFloatDrawableRect.bottom) {
            return EDGE_RB;
        } else if (mFloatDrawableRect.contains(eventX, eventY)) {
            return EDGE_MOVE_IN;
        }
        return EDGE_MOVE_OUT;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {

        if (mDrawable == null) {
            return;
        }

        if (mDrawable.getIntrinsicWidth() == 0 || mDrawable.getIntrinsicHeight() == 0) {
            return;
        }

        configureBounds();
        // 在画布上画图片
        mDrawable.draw(canvas);
        canvas.save();
        // 在画布上画浮层FloatDrawable,Region.Op.DIFFERENCE是表示Rect交集的补集
        canvas.clipRect(mDrawableFloat, Region.Op.DIFFERENCE);
        // 在交集的补集上画上灰色用来区分
        canvas.drawColor(Color.parseColor("#a0000000"));
        canvas.restore();
        // 画浮层
        mFloatDrawable.draw(canvas);
    }

    protected void configureBounds() {
        // configureBounds在onDraw方法中调用
        // isFirst的目的是下面对mDrawableSrc和mDrawableFloat只初始化一次,
        // 之后的变化是根据touch事件来变化的,而不是每次执行重新对mDrawableSrc和mDrawableFloat进行设置
        if (isFirst) {
            oriRationWH = ((float) mDrawable.getIntrinsicWidth())
                    / ((float) mDrawable.getIntrinsicHeight());

            final float scale = mContext.getResources().getDisplayMetrics().density;
            int mDrawableW = (int) (mDrawable.getIntrinsicWidth() * scale + 0.5f);
            if ((mDrawable.getIntrinsicHeight() * scale + 0.5f) > getHeight()) {
                mDrawableW = (int) ((mDrawable.getIntrinsicWidth() * scale + 0.5f)
                        * (getHeight() / (mDrawable.getIntrinsicHeight() * scale + 0.5f)));
            }
            int w = Math.min(getWidth(), mDrawableW);
            int h = (int) (w / oriRationWH);

            int left = (getWidth() - w) / 2;
            int top = (getHeight() - h) / 2;
            int right = left + w;
            int bottom = top + h;

            mDrawableSrc.set(left, top, right, bottom);
            mDrawableDst.set(mDrawableSrc);

            int floatWidth = dipToPx(mContext, cropWidth);
            int floatHeight = dipToPx(mContext, cropHeight);

            if (floatWidth > getWidth()) {
                floatWidth = getWidth();
                floatHeight = cropHeight * floatWidth / cropWidth;
            }

            if (floatHeight > getHeight()) {
                floatHeight = getHeight();
                floatWidth = cropWidth * floatHeight / cropHeight;
            }

            int floatLeft = (getWidth() - floatWidth) / 2;
            int floatTop = (getHeight() - floatHeight) / 2;
            mDrawableFloat.set(floatLeft, floatTop, floatLeft + floatWidth, floatTop + floatHeight);

            isFirst = false;
        } else if (getTouch((int) mX_1, (int) mY_1) == EDGE_MOVE_IN) {
            if (mDrawableFloat.left < 0) {
                mDrawableFloat.right = mDrawableFloat.width();
                mDrawableFloat.left = 0;
            }
            if (mDrawableFloat.top < 0) {
                mDrawableFloat.bottom = mDrawableFloat.height();
                mDrawableFloat.top = 0;
            }
            if (mDrawableFloat.right > getWidth()) {
                mDrawableFloat.left = getWidth() - mDrawableFloat.width();
                mDrawableFloat.right = getWidth();
            }
            if (mDrawableFloat.bottom > getHeight()) {
                mDrawableFloat.top = getHeight() - mDrawableFloat.height();
                mDrawableFloat.bottom = getHeight();
            }
            mDrawableFloat.set(mDrawableFloat.left, mDrawableFloat.top, mDrawableFloat.right,
                    mDrawableFloat.bottom);
        } else {
            if (mDrawableFloat.left < 0) {
                mDrawableFloat.left = 0;
            }
            if (mDrawableFloat.top < 0) {
                mDrawableFloat.top = 0;
            }
            if (mDrawableFloat.right > getWidth()) {
                mDrawableFloat.right = getWidth();
                mDrawableFloat.left = getWidth() - mDrawableFloat.width();
            }
            if (mDrawableFloat.bottom > getHeight()) {
                mDrawableFloat.bottom = getHeight();
                mDrawableFloat.top = getHeight() - mDrawableFloat.height();
            }
            mDrawableFloat.set(mDrawableFloat.left, mDrawableFloat.top, mDrawableFloat.right,
                    mDrawableFloat.bottom);
        }

        mDrawable.setBounds(mDrawableDst);
        mFloatDrawable.setBounds(mDrawableFloat);
    }

    // 进行图片的裁剪,所谓的裁剪就是根据Drawable的新的坐标在画布上创建一张新的图片
    public Bitmap getCropImage() {

        if (moverListner != null && mDrawableFloat!= null){
            moverListner.onImageCropcoordinate( mDrawableFloat.left,mDrawableFloat.top,mDrawableFloat.width(),mDrawableFloat.height());
        }

        Bitmap tmpBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.RGB_565);
        Canvas canvas = new Canvas(tmpBitmap);
        mDrawable.draw(canvas);

        Matrix matrix = new Matrix();
        float scale = (float) (mDrawableSrc.width())
                / (float) (mDrawableDst.width());
        matrix.postScale(scale, scale);

        Bitmap ret = Bitmap.createBitmap(tmpBitmap, mDrawableFloat.left,
                mDrawableFloat.top, mDrawableFloat.width(),
                mDrawableFloat.height(), matrix, true);

        PrintUtilsJava.pringtLog("cropImage:002   mDrawableFloat.left :" + mDrawableFloat.left);
        PrintUtilsJava.pringtLog("cropImage:002   mDrawableFloat.top :" + mDrawableFloat.top);
        PrintUtilsJava.pringtLog("cropImage:002   mDrawableFloat.width :" + mDrawableFloat.width());
        PrintUtilsJava.pringtLog("cropImage:002   mDrawableFloat.height() :" + mDrawableFloat.height());

        tmpBitmap.recycle();
        return ret;
    }

    public int dipToPx(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    public interface  OnFloatDrawableMoverListner{
        void onMoverListner();
        void onImageCropcoordinate(int left,int top,int width,int height);
    }

    private OnFloatDrawableMoverListner moverListner;
    public void setOnFloatDrawableMoverListner(OnFloatDrawableMoverListner ll){
        moverListner = ll;
    }
}

浮动移动框

package com.diction.app.android._av7.view.crop_iamge;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.RequiresApi;

public class FloatDrawable extends Drawable {

    private Context mContext;
    private int offset = 50;
    private Paint mLinePaint = new Paint();
    private Paint mLinePaint2 = new Paint();
    private Path path = new Path();
    private int lineLength = 40;
    private int panintWidth = 0;
    private int mPaddings = 15;

    {
        //设置Path

        mLinePaint.setARGB(200, 50, 50, 50);
        mLinePaint.setStrokeWidth(1F);
        mLinePaint.setStyle(Paint.Style.STROKE);
        mLinePaint.setAntiAlias(true);
        mLinePaint.setColor(Color.WHITE);
        //
        mLinePaint2.setARGB(200, 50, 50, 50);
         mLinePaint2.setStrokeWidth(10F);
        panintWidth = (int) mLinePaint2.getStrokeWidth();
        mLinePaint2.setStyle(Paint.Style.STROKE);
        mLinePaint2.setAntiAlias(true);
        mLinePaint2.setColor(Color.WHITE);
    }

    public FloatDrawable(Context context) {
        super();
        this.mContext = context;

    }

    public int getBorderWidth() {
        return dipToPx(mContext, offset);//根据dip计算的像素值,做适配用的
    }

    public int getBorderHeight() {
        return dipToPx(mContext, offset);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public void draw(Canvas canvas) {

        int left = getBounds().left;
        int top = getBounds().top;
        int right = getBounds().right;
        int bottom = getBounds().bottom;

        //
        canvas.drawLine(left+ mPaddings,top,left+lineLength+mPaddings,top,mLinePaint2);
        canvas.drawLine(left,top+ mPaddings,left,top+lineLength+mPaddings,mLinePaint2);
        canvas.drawArc(left,top,left+ mPaddings *2,top+ mPaddings *2,180,90,false,mLinePaint2);

        //
        canvas.drawLine(right- mPaddings-lineLength ,top,right- mPaddings,top,mLinePaint2);  //ok
        canvas.drawLine(right ,top+mPaddings,right,top+mPaddings+lineLength,mLinePaint2);  //ok
        canvas.drawArc(right-mPaddings *2,top,right,top+ mPaddings *2,270,90,false,mLinePaint2);


        canvas.drawLine(left,bottom-lineLength-mPaddings,left,bottom-mPaddings,mLinePaint2);
        canvas.drawLine(left+mPaddings,bottom,left+mPaddings+lineLength,bottom,mLinePaint2);
        canvas.drawArc(left,bottom-mPaddings*2,left+mPaddings*2,bottom,90,90,false,mLinePaint2);

        canvas.drawLine(right-lineLength-mPaddings,bottom,right-mPaddings,bottom,mLinePaint2);
        canvas.drawLine(right,bottom-mPaddings,right,bottom-mPaddings-lineLength,mLinePaint2);

        canvas.drawArc(right-mPaddings*2,bottom-mPaddings*2,right,bottom,0,90,false,mLinePaint2);

    }

    @Override
    public void setBounds(Rect bounds) {
//        super.setBounds(new Rect(bounds.left - dipToPx(mContext, offset) / 2,
//                bounds.top - dipToPx(mContext, offset) / 2,
//                bounds.right + dipToPx(mContext, offset) / 2,
//                bounds.bottom + dipToPx(mContext, offset) / 2));
        super.setBounds(new Rect(bounds.left ,
                bounds.top,
                bounds.right,
                bounds.bottom));
    }

    @Override
    public void setAlpha(int alpha) {

    }

    @Override
    public void setColorFilter(ColorFilter cf) {

    }

    @Override
    public int getOpacity() {
        return PixelFormat.UNKNOWN;
    }

    public int dipToPx(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
}

使用

cropimage.setDrawable(newBitmap, 300, 300)

你可能感兴趣的:(android,kotlin)