Android实现一个可以移动,删除,保存,自定义样式的便签app

    最近一直在研究安卓的自定义View的绘制,不过只是简单的单个模块练习远远达不到期望的水准,于是,决定通过完全的自定义View实现一个便签的app,一是为了提升自己对View绘制的掌握水平,二是这个app可以用来随时记录生活中的闪光点或者学习要点,在有空的时候对这些要点进行挖掘与回顾。

    app实现了便签的创建,保存,删除,多样式选择,详见gif:

    

便签的新建编辑与移动
便签的删除与保存后重新加载

 

app功能比较少,结构特别简单,主要包含5个模块,bean模块包含便签和样式,input模块包含对输入法的监听与便签内容的编辑,interface模块包含样式的定义,可以通过继承HtModel简单实现新的样式,utils一些工具类,具体可以看代码,view包含两个模块,最重要的是HtLayout,这是整个界面的主体,HtModel1-4是各种样式。

项目结构

 

 HtLayout是整个app的核心,代码已经做了很详细的注释了,可以很清晰的看到整个布局绘制及事件的脉络。HtLayout主要是两大功能,一个是各个模块的绘制工作,第二个是与用户的交互事件。

package com.jaden.htlabel.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;

import com.jaden.htlabel.bean.Label;
import com.jaden.htlabel.bean.Model;
import com.jaden.htlabel.input.HtLayoutInput;
import com.jaden.htlabel.interfaces.HtModel;
import com.jaden.htlabel.utils.DensityUtil;
import com.jaden.htlabel.utils.KeyBoardUtil;
import com.jaden.htlabel.utils.LabelUtil;
import com.jaden.htlabel.utils.ModelUtil;
import com.jaden.htlabel.utils.PointUtil;
import com.jaden.htlabel.utils.RectUtil;
import com.jaden.htlabel.utils.SqlUtil;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * Created Date: 2018/12/13
 * Description:
 */
public class HtLayout extends View{
    private static final int COLOR_EDIT = Color.parseColor("#45344533");
    private static final float MODEL_HEIGHT = DensityUtil.dp2px(50); //50dp
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    //整个布局分为两个部分,一个是专门用来展示label的范围,一个是专门用来展示样式的范围
    private RectF mLabelRect = new RectF(); //label的范围
    private RectF mModelRect = new RectF(); //model的范围

    private Rect mDirtyRect = new Rect(); //绘制的脏区域

    private List<Label> labels = new LinkedList<>();//由于便签经常会被删除及新增,用LinkedList提升效率
    private Label selectedLabel = null;

    private List<HtModel> models = new ArrayList<>();
    private HtModel selectModel = null;

    private RectF mBackground = new RectF(); //label的范围
    private Rect mTextBonds = new Rect(); //label文字的范围
    private Rect mDateBonds = new Rect(); //label时间的范围

    private PointF currentPoint = new PointF(); //当前的touch区域
    private PointF lastPoint = new PointF(); //上一次的touch区域

    private boolean moveFlag = false; //选中的label是否可以移动
    private boolean clickFlag = false; //选中的label是否点击

    HtLayoutInput ic;

    public HtLayout(Context context) {
        super(context);
    }

    public HtLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public HtLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
    *description: 绘制label列表
    **/
    public void setLabels(List<Label> labels) {
        if(labels != null) {
            this.labels.clear();
            this.labels.addAll(labels);
            invalidate();
        }
    }

    /**
    *description: 保存当前选中的label
    **/
    private void setLabel(Label label) {
        this.selectedLabel = label;
    }

    /**
    *description: 绘制HtModel列表
    **/
    public void setModels(List<HtModel> models) {
        if(models != null) {
            this.models.clear();
            this.models.addAll(models);
            invalidate();
        }
    }

    /**
    *description: 移动label,刷新单个label,提升效率
    **/
    private void moveLabel(Label label, PointF current, PointF last){
        float oldX = label.getX();
        float oldY = label.getY();
        float newX = oldX + current.x - last.x;
        float newY = oldY + current.y - last.y;
        label.setX(newX);
        label.setY(newY);
        invalidate(label);
    }

    /**
    *description: 点击label事件
    **/
    private void clickLabel(Label label){
        label.setEditable(true);
        invalidate(label);

        //输入法相关,使其能够与view建立联系
        if(ic != null && selectedLabel != null){
            ic.setLabel(selectedLabel);
        }
        KeyBoardUtil.toggleSoftInput(this);
        setFocusableInTouchMode(true);
        setFocusable(true);
        requestFocus();
    }

    /**
    *description: 绘制单个label
    **/
    private void drawLabel(Label label, Canvas canvas){
        mPaint.setColor(label.getBackgroundColor());
        mPaint.setStyle(Paint.Style.FILL);
        //求取一些参数
        mPaint.setTextSize(label.getTextSize());
        mPaint.getTextBounds(label.getText(), 0, label.getText().length(), mTextBonds);
        float textWidth = mTextBonds.right-mTextBonds.left;
        float textHeight = mTextBonds.bottom-mTextBonds.top;
        mPaint.setTextSize(label.getDateSize());
        mPaint.getTextBounds(label.getDate(), 0, label.getDate().length(), mDateBonds);
        float dateWidth = mDateBonds.right-mDateBonds.left;
        float dateHeight = mDateBonds.bottom-mDateBonds.top;
        float wMax = Math.max(textWidth, dateWidth);
        //绘制背景
        float w = label.getSpaceX()*2 + wMax; //space + max(date, text) + space
        float h = label.getSpaceY()*3 + dateHeight + textHeight; //space + date + space + text + space
        label.setW(w);
        label.setH(h);
        LabelUtil.setRectFByLabel(mBackground, label);
        canvas.drawRect(mBackground, mPaint);
        //绘制日期
        mPaint.setColor(label.getDateColor());
        canvas.drawText(label.getDate(), mBackground.left + (wMax-dateWidth)/2 + label.getSpaceX(), mBackground.top + label.getSpaceY() - mDateBonds.top, mPaint);
        //绘制文字
        mPaint.setColor(label.getTextColor());
        mPaint.setTextSize(label.getTextSize());
        float textOffsetX = mBackground.left + (wMax-textWidth)/2 + label.getSpaceX();
        float textOffsetY = mBackground.bottom - label.getSpaceY() - mTextBonds.bottom;
        canvas.drawText(label.getText(), textOffsetX, textOffsetY, mPaint);
        //绘制可编辑的蒙版
        if(label.getEditable()){ //如果可编辑,那么就将文字表面附上一层蒙版
            mPaint.setColor(COLOR_EDIT);
            canvas.drawRect(mTextBonds.left + textOffsetX, mTextBonds.top + textOffsetY,
                    mTextBonds.right + textOffsetX, mTextBonds.bottom + textOffsetY, mPaint);
        }
    }

    /**
    *description: 绘制所有的label
    **/
    private void drawLabels(List<Label> labels, Canvas canvas){
        for(int i=0; i<labels.size(); i++){
            Label label = labels.get(i);
            label.setzOrder(i);
            drawLabel(label, canvas);
        }
    }

    /**
    *description: 绘制所有的样式
    **/
    private void drawModels(List<HtModel> htModels, Canvas canvas){
        int size = htModels.size();
        if(size > 0) {
            float w = getWidth() * 1.0f / size;
            float h = MODEL_HEIGHT;
            for (int i = 0; i < size; i++) {
                HtModel model = htModels.get(i);
                drawModel(model.createModel(new RectF(i*w, getHeight() - h, (i+1)*w, getHeight())), canvas);
            }
        }
    }

    /**
    *description: 绘制单个样式
    **/
    private void drawModel(Model model, Canvas canvas){
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(model.getBgColor());
        canvas.drawRect(model.getRectF(), mPaint);
        mPaint.setTextSize(model.getTextSize());
        mPaint.setColor(model.getTextColor());
        mPaint.getTextBounds(model.getText(), 0, model.getText().length(), mTextBonds);
        float x = model.getRectF().left + ((model.getRectF().right - model.getRectF().left) - (mTextBonds.right - mTextBonds.left))/2 + mTextBonds.left;
        float y = model.getRectF().top + ((model.getRectF().bottom - model.getRectF().top) - (mTextBonds.bottom - mTextBonds.top))/2 - mTextBonds.top;
        canvas.drawText(model.getText(), x, y, mPaint);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mLabelRect.set(0, 0, getWidth(), getHeight()-MODEL_HEIGHT);
        mModelRect.set(0, getHeight()-MODEL_HEIGHT, getWidth(), getHeight());
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    if(RectUtil.inRegion(mLabelRect, event)) {//点击label区域
                        if (selectedLabel != null) {
                            selectedLabel.setEditable(false);
                        }
                        selectedLabel = LabelUtil.getSelectedLabels(labels, event);
                        if (selectedLabel != null) {
                            PointUtil.motion2point(lastPoint, event);
                            moveFlag = true;
                            clickFlag = true;
                        } else {
                            KeyBoardUtil.hideKeyboard(this);
                        }
                    }else if(RectUtil.inRegion(mModelRect, event)){ //HTModel区域
                        selectModel = ModelUtil.getRegionHtModel(models, event);
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (moveFlag && selectedLabel != null) {
                        PointUtil.motion2point(currentPoint, event);
                        if (PointUtil.isMove(currentPoint, lastPoint, 5)) { //判断是否移动
                            clickFlag = false;
                            moveLabel(selectedLabel, currentPoint, lastPoint);
                            lastPoint.set(currentPoint);
                        }
                    }
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    if (moveFlag && selectedLabel != null) {
                        if (clickFlag) {//点击标签事件
                            clickLabel(selectedLabel);
                        } else {//移动标签事件
                            if (LabelUtil.isOutOfView(mLabelRect, selectedLabel)) { //移到View外
                                if(labels.remove(selectedLabel)) {
                                    SqlUtil.get().saveObject(this.labels, SqlUtil.LABEL_LIST);
                                }
                                invalidate();
                            }
                        }
                    }
                    if(selectModel != null && !RectUtil.inRegion(mModelRect, event)){ //如果手指离开的时候,不在HTModel区域,那么就创建一个label标签
                        Label label = selectModel.createLabel(event);
                        labels.add(label);
                        invalidate();
                    }
                    selectModel = null;
                    moveFlag = false;
                    break;
            }
        return true;
    }

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(this.models != null) {
            drawModels(this.models, canvas);
        }
        if(this.labels != null) {
            drawLabels(this.labels, canvas);
            SqlUtil.get().saveObject(this.labels, SqlUtil.LABEL_LIST);
        }
    }

    @Override
    public boolean onCheckIsTextEditor() {
        return true;
    }

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI;
        outAttrs.inputType = InputType.TYPE_NULL;
        ic = new HtLayoutInput(this, true);
        ic.setLabel(selectedLabel); //初始化的时候绑定Label
        ic.setListener(new HtLayoutInput.Listener() {
            @Override
            public void onInputComplete() {
                invalidate(selectedLabel);
            }

            @Override
            public void onDelComplete() {
                invalidate(selectedLabel);
            }
        });
        return ic;
    }

    /**
    *description: 局部更新
    **/
    private void invalidate(Label label){
        LabelUtil.setRectFByLabel(mBackground, label);
        mBackground.roundOut(mDirtyRect); //确保脏区域比矩形区域大
        invalidate(mDirtyRect);
    }

    /**
    *description: 隐藏keyboard
    **/
    public void detachView(){
        KeyBoardUtil.hideKeyboard(this);
    }

}

整个绘制分为两大部分,一个是上面的标签区域的绘制,第二个是最下面样式的绘制。

标签区域实际上是所有标签的依次绘制,对于单个label标签而言,分为主体背景,上面的时间,下面的标签内容三个部分,如果标签被选中,那么则会在标签内容上覆盖一个Rect的蒙板,通过对label各参数的解析,可以完成从上到下的依次绘制。

样式的绘制更加简单,只用绘制Rect背景区域以及文字。

 

与用户的交互,主要分四个部分:

第一个是新建label时候,从样式滑动到label主体这个事件,在事件结束后新建一个label,插入到数据库。

第二个是label的自由滑动,这个只用改变label的坐标,然后注意重叠时候的绘制次序即可,不过需要注意的是,如果多个label重叠,那么选中重叠区域的时候,要对label的zOrder做一个判断,返回最上面的label给用户操作。

第三个是label的删除,如果移出了标签区域,那么就将label从List里面删除,保存数据库即可。

第四个是label数据的编辑,这个是通过继承BaseInputConnection来实现对软键盘事件及字符串输出的监听。

 

整个app的大概脉络已经讲清楚了,具体的可以参考源代码进行阅读。

好的,下面我会提供一个简单的步骤来帮助你创建一个基本的便签应用程序: 1. 创建一个新的Android Studio项目 2. 在布局文件中添加一个EditText视图和一个Button视图,以便用户可以输入和保存笔记。你可以使用以下代码来创建一个简单的布局: ``` <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <EditText android:id="@+id/noteEditText" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Enter your note here"/> <Button android:id="@+id/saveButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Save" android:layout_below="@id/noteEditText" android:layout_alignParentRight="true" android:layout_marginTop="16dp" android:layout_marginRight="16dp"/> </RelativeLayout> ``` 3. 在MainActivity类中,定义EditText和Button的变量,并在onCreate方法中初始化它们: ``` public class MainActivity extends AppCompatActivity { private EditText noteEditText; private Button saveButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); noteEditText = findViewById(R.id.noteEditText); saveButton = findViewById(R.id.saveButton); } } ``` 4. 在saveButton上添加点击事件,以便在用户点击"保存"按钮时保存笔记。你可以使用以下代码来实现这一点: ``` saveButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { String note = noteEditText.getText().toString(); // Save the note to a file or database } }); ``` 5. 最后,你需要添加适当的权限,以便你的应用程序可以访问文件系统或数据库。在AndroidManifest.xml文件中添加以下权限: ``` <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> ``` 这就是创建一个基本的便签应用程序的主要步骤。当然,你可以根据自己的需求添加更多的功能,例如笔记列表、编辑笔记、删除笔记等等。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值