【Android、IOS、Flutter、鸿蒙、ReactNative 】编辑框

161 阅读7分钟

Android Java 编辑框

自定义编辑框 参考博客园参考CSDN

import android.content.Context;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.CycleInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.EditText;
import java.util.jar.Attributes;
public class ClearEditText extends EditText implements View.OnFocusChangeListener, TextWatcher {
    //删除按钮的引用
    private Drawable mClearDrawable;
    private Context context;

    //控件是否有焦点
    private boolean hasFocus;

    public ClearEditText(Context context) {
        this(context, null);
    }

    public ClearEditText(Context context, AttributeSet attrs) {
        //这里构造方法也很重要,不加这个很多属性不能再XML里面定义
        this(context, attrs, android.R.attr.editTextStyle);
    }

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

    private void init() {
        //获取EditText的DrawableRight,假如没有设置我们就使用默认的图片
        mClearDrawable = getCompoundDrawables()[2];
        if (mClearDrawable == null) {
            mClearDrawable = getResources().getDrawable(R.drawable.delete_selector);
        }
        mClearDrawable.setBounds(0, 0, mClearDrawable.getIntrinsicWidth(), mClearDrawable.getIntrinsicHeight());
        //默认设置隐藏图标
        setClearIconVisible(false);
        //设置焦点改变的监听
        setOnFocusChangeListener(this);
        //设置输入框里面内容发生改变的监听
        addTextChangedListener(this);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mClearDrawable != null && event.getAction() == MotionEvent.ACTION_UP) {
            int x = (int) event.getX();
            //判断触摸点是否在水平范围内
            boolean isInnerWidth = (x > (getWidth() - getTotalPaddingRight())) &&
                    (x < (getWidth() - getPaddingRight()));
            //获取删除图标的边界,返回一个Rect对象
            Rect rect = mClearDrawable.getBounds();
            //获取删除图标的高度
            int height = rect.height();
            int y = (int) event.getY();
            /计算图标底部到控件底部的距离
            int distance = (getHeight() - height) / 2;
            //判断触摸点是否在竖直范围内(可能会有点误差)
            //触摸点的纵坐标在distance到(distance+图标自身的高度)之内,则视为点中删除图标
            boolean isInnerHeight = (y > distance) && (y < (distance + height));
            if (isInnerHeight && isInnerWidth) {
                this.setText("");
            }
        }
        return super.onTouchEvent(event);
    }

    /**
     * 设置清除图标的显示与隐藏,调用setCompoundDrawables为EditText绘制上去
     *
     * @param visible
     */
    private void setClearIconVisible(boolean visible) {
        Drawable right = visible ? mClearDrawable : null;
        setCompoundDrawables(getCompoundDrawables()[0], getCompoundDrawables()[1],
                right, getCompoundDrawables()[3]);
    }

    /**
     * 当ClearEditText焦点发生变化的时候,判断里面字符串长度设置清除图标的显示与隐藏
     */
    @Override
    public void onFocusChange(View v, boolean hasFocus) {
        this.hasFocus = hasFocus;
        if (hasFocus) {
            setClearIconVisible(getText().length() > 0);
        } else {
            setClearIconVisible(false);
        }
    }

    /**
     * 当输入框里面内容发生变化的时候回调的方法
     */
    @Override
    public void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        if (hasFocus) {
            setClearIconVisible(text.length() > 0);
        }
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void afterTextChanged(Editable s) {

    }

    /**
     * 设置晃动动画
     */
    public void setShakeAnimation() {
        this.startAnimation(shakeAnimation(5));
    }

    /**
     * 晃动动画
     *
     * @param counts 1秒钟晃动多少下
     * @return
     */
    public static Animation shakeAnimation(int counts) {
        Animation translateAnimation = new TranslateAnimation(0, 10, 0, 0);
        translateAnimation.setInterpolator(new CycleInterpolator(counts));
        translateAnimation.setDuration(1000);
        return translateAnimation;
    }
}

使用编辑框

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <com.java.editbox.ClearEditText
        android:id="@+id/username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="60dp"
        android:layout_marginRight="10dp"
        android:background="@drawable/login_edittext_bg"
        android:drawableLeft="@drawable/icon_user"
        android:drawableRight="@drawable/delete_selector"
        android:hint="输入用户名"
        android:singleLine="true"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <TextView
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:background="@drawable/login_button_bg"
        android:text="登录"
        android:gravity="center"
        android:textColor="@android:color/white"
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintRight_toRightOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private Toast mToast;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final ClearEditText username = (ClearEditText) findViewById(R.id.username);
        final TextView mButton = (TextView) findViewById(R.id.login);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (TextUtils.isEmpty(username.getText())) {
                    //设置晃动
                    username.setShakeAnimation();
                    //设置提示
                    showToast("用户名不能为空!");
                }
            }
        });
    }

    // 显示Toast消息
    private void showToast(String msg) {
        if (mToast == null) {
            mToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
        } else {
            mToast.setText(msg);
        }
        mToast.show();
    }
}

Android Kotlin 编辑框

import android.content.Context
import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.View.OnFocusChangeListener
import android.view.animation.Animation
import android.view.animation.CycleInterpolator
import android.view.animation.TranslateAnimation
import androidx.appcompat.widget.AppCompatEditText


class ClearEditText @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyle: Int = android.R.attr.editTextStyle
) :
    AppCompatEditText(context!!, attrs, defStyle), OnFocusChangeListener, TextWatcher {
    //删除按钮的引用
    private var mClearDrawable: Drawable? = null
    private val context: Context? = null

    //控件是否有焦点
    private var hasFocus = false

    init {
        init()
    }

    private fun init() {
        //获取EditText的DrawableRight,假如没有设置我们就使用默认的图片
        mClearDrawable = compoundDrawables[2]
        if (mClearDrawable == null) {
            mClearDrawable = resources.getDrawable(R.drawable.delete_selector)
        }
        mClearDrawable?.setBounds(
            0,
            0,
            mClearDrawable!!.intrinsicWidth,
            mClearDrawable!!.intrinsicHeight
        )
        //默认设置隐藏图标
        setClearIconVisible(false)
        //设置焦点改变的监听
        onFocusChangeListener = this
        //设置输入框里面内容发生改变的监听
        addTextChangedListener(this)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (mClearDrawable != null && event.action == MotionEvent.ACTION_UP) {
            val x = event.x.toInt()
            //判断触摸点是否在水平范围内
            val isInnerWidth = x > width - totalPaddingRight && x < width - paddingRight
            //获取删除图标的边界,返回一个Rect对象
            val rect = mClearDrawable!!.bounds
            //获取删除图标的高度
            val height = rect.height()
            val y = event.y.toInt()
            //计算图标底部到控件底部的距离
            val distance = (getHeight() - height) / 2
            //判断触摸点是否在竖直范围内(可能会有点误差)
            //触摸点的纵坐标在distance到(distance+图标自身的高度)之内,则视为点中删除图标
            val isInnerHeight = y > distance && y < distance + height
            if (isInnerHeight && isInnerWidth) {
                this.setText("")
            }
        }
        return super.onTouchEvent(event)
    }

    /**
     * 设置清除图标的显示与隐藏,调用setCompoundDrawables为EditText绘制上去
     *
     * @param visible
     */
    private fun setClearIconVisible(visible: Boolean) {
        val right = if (visible) mClearDrawable else null
        setCompoundDrawables(
            compoundDrawables[0], compoundDrawables[1],
            right, compoundDrawables[3]
        )
    }

    /**
     * 当ClearEditText焦点发生变化的时候,判断里面字符串长度设置清除图标的显示与隐藏
     */
    override fun onFocusChange(v: View, hasFocus: Boolean) {
        this.hasFocus = hasFocus
        if (hasFocus) {
            setClearIconVisible(text?.length!! > 0)
        } else {
            setClearIconVisible(false)
        }
    }

    /**
     * 当输入框里面内容发生变化的时候回调的方法
     */
    override fun onTextChanged(
        text: CharSequence,
        start: Int,
        lengthBefore: Int,
        lengthAfter: Int
    ) {
        if (hasFocus) {
            setClearIconVisible(text.length > 0)
        }
    }

    override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
    override fun afterTextChanged(s: Editable) {}

    /**
     * 设置晃动动画
     */
    fun setShakeAnimation() {
        startAnimation(shakeAnimation(5))
    }

    companion object {
        /**
         * 晃动动画
         *
         * @param counts 1秒钟晃动多少下
         * @return
         */
        fun shakeAnimation(counts: Int): Animation {
            val translateAnimation: Animation = TranslateAnimation(0f, 10f, 0f, 0f)
            translateAnimation.interpolator = CycleInterpolator(counts.toFloat())
            translateAnimation.duration = 1000
            return translateAnimation
        }
    }
}

Android Compose 编辑框

导入依赖包

dependencies {

    ......
    implementation ("androidx.activity:activity-compose:1.3.1")
    implementation("androidx.compose.material:material:1.4.3")
    implementation("androidx.compose.ui:ui-tooling:1.4.3")
}

启用Compose

image.png

Compose 自定义编辑框

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp


class MainActivity : ComponentActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            var shake by remember { mutableStateOf(false) }
            val transition = updateTransition(targetState = shake, label = "shake")
            val shakeOffset by transition.animateDp(label = "",
                transitionSpec = {
                    keyframes {
                        durationMillis = 300
                        0.dp at 0
                        (-10).dp at 25 with LinearOutSlowInEasing
                        0.dp at 50
                        10.dp at 75
                        0.dp at 100
                        (-8).dp at 125
                        0.dp at 150
                        8.dp at 175
                        0.dp at 200
                        (-5).dp at 225
                        0.dp at 250
                        5.dp at 275
                        0.dp at 300
                    }
                }) {
                if (it) 0.dp else 0.dp
            }
            Column {
                DecorateTextField(shakeOffset = shakeOffset)
                RoundedCornerClickText(onClick = {
                    shake = !shake
                })
            }
        }
    }

    @Composable
    fun DecorateTextField(shakeOffset: Dp) {
        var text by rememberSaveable {
            mutableStateOf("")
        }
        Box(
            Modifier
                .padding(start = 10.dp, end = 10.dp, top = 20.dp)
                .fillMaxWidth()
                .offset(x = shakeOffset),
            contentAlignment = Alignment.TopCenter
        ) {
            BasicTextField(
                value = text,
                onValueChange = {
                    text = it
                },
                textStyle = TextStyle(color = Color.Black),
                cursorBrush = SolidColor(Color.Blue),
                decorationBox = { innerTextField ->//decorationBox内部负责编写输入框样式
                    Row(
                        Modifier
                            .fillMaxWidth()
                            .height(50.dp)
                            .border(0.3.dp, Color.Blue, RoundedCornerShape(10.dp)),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Spacer(Modifier.width(5.dp))
                        Icon(
                            Icons.Default.AccountCircle,
                            tint = Color.Black,
                            contentDescription = null
                        )
                        Spacer(Modifier.width(5.dp))
                        Box(modifier = Modifier.padding(top = 7.dp, bottom = 7.dp, end = 7.dp)) {
                            // 判断是否输入文本,如果输入则清空隐藏提示
                            if (text.isEmpty()) {
                                Text(
                                    text = "请输入搜索内容",
                                    style = TextStyle(
                                        color = Color(0, 0, 0, 128),
                                        fontSize = 16.sp,
                                    )
                                )
                            }
                            //自定义样式这行代码是关键,没有这一行输入文字后无法展示,光标也看不到
                            innerTextField()
                        }
                    }
                }
            )
            // 编辑框存在文本值
            if (text.isNotEmpty()) {
                // 添加一个清除按钮,点击时清除文本
                val interactionSource = remember { MutableInteractionSource() }
                Box(
                    modifier = Modifier
                        .align(Alignment.CenterEnd)
                        .clickable(
                            interactionSource = interactionSource,
                            indication = null
                        ) {
                            text = ""
                        }
                        .padding(end = 8.dp) // 根据需要调整内边距
                ) {
                    Icon(
                        imageVector = Icons.Default.Close, // 使用合适的图标
                        contentDescription = "clear",
                        modifier = Modifier.size(24.dp), // 根据需要调整图标大小
                        tint = Color.Unspecified // 根据需要调整颜色
                    )
                }
            }
        }
    }

    @Composable
    fun RoundedCornerClickText(onClick: () -> Unit) {
        Box(modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 40.dp)) {
            Button(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(50.dp),
                onClick = onClick,
                shape = RoundedCornerShape(5.dp),
                colors = ButtonDefaults.buttonColors(Color.Green)
            ) {
                Text(text = "登录", style = TextStyle(color = Color.White))
            }
        }
    }
}

IOS Object-c 编辑框

自定义编辑框

image.png

使用编辑框

image.png

抖动动画

image.png

IOS Swift 编辑框

自定义编辑框

image.png

使用编辑框

image.png

抖动动画

image.png

Flutter 编辑框

import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

const shakeCount = 4;
const shakeDuration = Duration(milliseconds: 600);

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  //定义一个controller
  final TextEditingController _editController = TextEditingController();

  // 显示一键删除按钮
  bool visible = false;

  late final AnimationController _shakeController =
      AnimationController(vsync: this, duration: shakeDuration);

  @override
  void initState() {
    _shakeController.addListener(() {
      if (_shakeController.status == AnimationStatus.completed) {
        _shakeController.reset();
      }
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: Text(widget.title),
        ),
        body: Column(
          children: [
            AnimatedBuilder(
                animation: _shakeController,
                builder: (context, child) {
                  final sineValue =
                      sin(shakeCount * 2 * pi * _shakeController.value);
                  return Transform.translate(
                    offset: Offset(sineValue * 10, 0),
                    child: child,
                  );
                },
                child: _editBox()),
            _loginButton(),
          ],
        ));
  }

  Widget _editBox() {
    return Container(
        margin: const EdgeInsets.only(left: 10.0, right: 10.0, top: 20.0),
        padding: const EdgeInsets.only(left: 10.0),
        height: 50.0,
        decoration: BoxDecoration(
            color: Colors.transparent,
            border: Border.all(color: Colors.purple, width: 0.6),
            borderRadius: const BorderRadius.all(Radius.circular(10.0))),
        child: Row(
          children: [
            const Icon(Icons.supervisor_account),
            Expanded(
              flex: 1,
              child: TextField(
                autofocus: true,
                decoration: null,
                onChanged: (v) {
                  if (kDebugMode) {
                    print('编辑框的值:$v');
                  }
                  _changeVisible();
                },
                controller: _editController, //设置controller
              ),
            ),
            Visibility(
              visible: visible,
              child: GestureDetector(
                onTap: () {
                  _editController.clear();
                  _changeVisible();
                },
                child: Container(
                  alignment: Alignment.center,
                  width: 40.0,
                  height: 40.0,
                  color: Colors.transparent,
                  child: const Icon(Icons.close),
                ),
              ),
            )
          ],
        ));
  }

  void _changeVisible() {
    // 获取编辑框文本值
    var v = _editController.value.text;
    // 编辑框文本值长度小于等于1刷新界面
    if (!visible || v.length <= 1) {
      setState(() {
        visible = (v.isNotEmpty);
      });
    }
  }

  Widget _loginButton() {
    return GestureDetector(
      onTap: () {
        _shakeController.reset();
        _shakeController.forward();
      },
      child: Container(
        width: double.infinity,
        alignment: Alignment.center,
        height: 50.0,
        margin: const EdgeInsets.only(left: 10.0, right: 10.0, top: 50.0),
        decoration: BoxDecoration(
            color: Colors.lightBlue, borderRadius: BorderRadius.circular(10.0)),
        child: const Text(
          '登录',
          style: TextStyle(color: Colors.white),
        ),
      ),
    );
  }
}

ReactNative 编辑框

import React, {Component,useRef,useEffect} from 'react';
import { AppRegistry,TextInput, Button,View,
        StyleSheet,Image,TouchableOpacity,
        Text,Animated,Easing} from 'react-native';
import {name as appName} from './app.json';

export default class TextInputComponent extends Component {

    constructor(props) {
        super(props);
        this.state = {
            inputValue:'' // 编辑框初始化值
        }
    }

    // 清空文本
    clearText=()=> {
        this.setState({inputValue:''});
    }

    render() {
        return (
            <View style={styles.editView}>
                <Image source={require('./assets/user_logo.png')} style={styles.editViewImage}/>
                <TextInput
                        style={styles.input}
                        value={this.state.inputValue}
                        onChangeText={(text)=>{this.setState({inputValue:text})}}
                        placeholder="请输入编辑框内容"
                />
                {this.state.inputValue && (<TouchableOpacity onPress={this.clearText}>
                        <Image
                            source={require('./assets/edit_delete.png')}
                            style={styles.deleteViewImage}
                            />
                      </TouchableOpacity>
                )}
            </View>
     )
  }
}

// 抖动动画
const ShakeAnimation = () => {
   const shakeAnimation = useRef(new Animated.Value(0)).current;
   shake = () => {
      // 定义动画:在X轴左右抖动
      Animated.sequence([
        Animated.timing(shakeAnimation, {
          toValue: 3,
          duration: 100,//毫秒
          useNativeDriver: true,//使用原生动画驱动,默认不启用(false)
        }),
        Animated.timing(shakeAnimation, {
          toValue: -3,
          duration: 100,//毫秒
          useNativeDriver: true,
        }),
        Animated.spring(shakeAnimation, {
          toValue: 0,
          useNativeDriver: true,
        }),
      ]).start(); // 开始动画
    };

  // 应用动画值
  const shakeStyle = {
    transform: [
      {
        translateX: shakeAnimation.interpolate({
          inputRange: [0, 1],
          outputRange: [0, 3], // 抖动的幅度
        }),
      },
    ],
  };

  return (<View style={styles.container}>
            <Animated.View style={shakeStyle}>
                <TextInputComponent/>
            </Animated.View>
            <Button title="登录" color="blue" onPress={this.shake}/>
         </View>);
};


const styles = StyleSheet.create({
  editViewImage:{
    width:25,
    height:25,
    marginLeft:10,
  },
  deleteViewImage:{
      width:25,
      height:25,
      marginRight:10,
  },
  editView:{
     flexDirection: 'row',
     alignItems: 'center',
     marginBottom: 40,
     borderWidth: 1,
     borderColor: 'gray',
     borderRadius: 10,
  },
  container: {
    flex: 1,
    justifyContent: 'top',
    padding: 20,
  },
  input: {
    height: 45,
    padding: 5,
    flex: 1,
  },
});

AppRegistry.registerComponent(appName, () => ShakeAnimation);

案例

切换到分支 flutter_editbox

image.png