• 介绍几个工作开发中封装的好用的android自定义控件


    首先看效果图,

    看下这两个界面,第一个中用到了一个自定义的FlowRadioGroup,支持复合子控件,自定义布局;

    第二个界面中看到了输入的数字 自动4位分割了吧;也用到了自定义的DivisionEditText控件。

    下面直接看源码FlowRadioGroup了;

      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.newgame.sdk.view;
     18 
     19 import java.util.ArrayList;
     20 
     21 import android.content.Context;
     22 import android.content.res.TypedArray;
     23 import android.util.AttributeSet;
     24 import android.view.View;
     25 import android.view.ViewGroup;
     26 import android.widget.CompoundButton;
     27 import android.widget.LinearLayout;
     28 import android.widget.RadioButton;
     29 
     30 /** 可以放多种布局控件,能找到radiobutton */
     31 public class FlowRadioGroup extends LinearLayout {
     32     // holds the checked id; the selection is empty by default
     33     private int mCheckedId = -1;
     34     // tracks children radio buttons checked state
     35     private CompoundButton.OnCheckedChangeListener mChildOnCheckedChangeListener;
     36     // when true, mOnCheckedChangeListener discards events
     37     private boolean mProtectFromCheckedChange = false;
     38     private OnCheckedChangeListener mOnCheckedChangeListener;
     39     private PassThroughHierarchyChangeListener mPassThroughListener;
     40 
     41     // 存放当前的radioButton
     42     private ArrayList<RadioButton> radioButtons;
     43 
     44     public FlowRadioGroup(Context context) {
     45         super(context);
     46         setOrientation(VERTICAL);
     47         init();
     48     }
     49 
     50     public FlowRadioGroup(Context context, AttributeSet attrs) {
     51         super(context, attrs);
     52         init();
     53     }
     54 
     55     private void init() {
     56         mChildOnCheckedChangeListener = new CheckedStateTracker();
     57         mPassThroughListener = new PassThroughHierarchyChangeListener();
     58         super.setOnHierarchyChangeListener(mPassThroughListener);
     59         radioButtons = new ArrayList<RadioButton>();
     60     }
     61 
     62     @Override
     63     public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
     64         // the user listener is delegated to our pass-through listener
     65         mPassThroughListener.mOnHierarchyChangeListener = listener;
     66     }
     67 
     68     @Override
     69     protected void onFinishInflate() {
     70         super.onFinishInflate();
     71 
     72         // checks the appropriate radio button as requested in the XML file
     73         if (mCheckedId != -1) {
     74             mProtectFromCheckedChange = true;
     75             setCheckedStateForView(mCheckedId, true);
     76             mProtectFromCheckedChange = false;
     77             setCheckedId(mCheckedId);
     78         }
     79     }
     80 
     81     @Override
     82     public void addView(View child, int index, ViewGroup.LayoutParams params) {
     83         if (child instanceof RadioButton) {
     84             final RadioButton button = (RadioButton) child;
     85             radioButtons.add(button);
     86 
     87             if (button.isChecked()) {
     88                 mProtectFromCheckedChange = true;
     89                 if (mCheckedId != -1) {
     90                     setCheckedStateForView(mCheckedId, false);
     91                 }
     92                 mProtectFromCheckedChange = false;
     93                 setCheckedId(button.getId());
     94             }
     95         } else if (child instanceof ViewGroup) {// 如果是复合控件
     96             // 遍历复合控件
     97             ViewGroup vg = ((ViewGroup) child);
     98             setCheckedView(vg);
     99         }
    100 
    101         super.addView(child, index, params);
    102     }
    103 
    104     /** 查找复合控件并设置radiobutton */
    105     private void setCheckedView(ViewGroup vg) {
    106         int len = vg.getChildCount();
    107         for (int i = 0; i < len; i++) {
    108             if (vg.getChildAt(i) instanceof RadioButton) {// 如果找到了,就设置check状态
    109                 final RadioButton button = (RadioButton) vg.getChildAt(i);
    110                 // 添加到容器
    111                 radioButtons.add(button);
    112                 if (button.isChecked()) {
    113                     mProtectFromCheckedChange = true;
    114                     if (mCheckedId != -1) {
    115                         setCheckedStateForView(mCheckedId, false);
    116                     }
    117                     mProtectFromCheckedChange = false;
    118                     setCheckedId(button.getId());
    119                 }
    120             } else if (vg.getChildAt(i) instanceof ViewGroup) {// 迭代查找并设置
    121                 ViewGroup childVg = (ViewGroup) vg.getChildAt(i);
    122                 setCheckedView(childVg);
    123             }
    124         }
    125     }
    126 
    127     /** 查找复合控件并设置id */
    128     private void setCheckedId(ViewGroup vg) {
    129         int len = vg.getChildCount();
    130         for (int i = 0; i < len; i++) {
    131             if (vg.getChildAt(i) instanceof RadioButton) {// 如果找到了,就设置check状态
    132                 final RadioButton button = (RadioButton) vg.getChildAt(i);
    133                 int id = button.getId();
    134                 // generates an id if it's missing
    135                 if (id == View.NO_ID) {
    136                     id = button.hashCode();
    137                     button.setId(id);
    138                 }
    139                 button.setOnCheckedChangeListener(mChildOnCheckedChangeListener);
    140             } else if (vg.getChildAt(i) instanceof ViewGroup) {// 迭代查找并设置
    141                 ViewGroup childVg = (ViewGroup) vg.getChildAt(i);
    142                 setCheckedId(childVg);
    143             }
    144         }
    145     }
    146 
    147     /** 查找radioButton控件 */
    148     public RadioButton findRadioButton(ViewGroup group) {
    149         RadioButton resBtn = null;
    150         int len = group.getChildCount();
    151         for (int i = 0; i < len; i++) {
    152             if (group.getChildAt(i) instanceof RadioButton) {
    153                 resBtn = (RadioButton) group.getChildAt(i);
    154             } else if (group.getChildAt(i) instanceof ViewGroup) {
    155                 resBtn = findRadioButton((ViewGroup) group.getChildAt(i));
    156                 findRadioButton((ViewGroup) group.getChildAt(i));
    157                 break;
    158             }
    159         }
    160         return resBtn;
    161     }
    162 
    163     /** 返回当前radiobutton控件的count */
    164     public int getRadioButtonCount() {
    165         return radioButtons.size();
    166     }
    167 
    168     /** 返回当前index的radio */
    169     public RadioButton getRadioButton(int index) {
    170         return radioButtons.get(index);
    171     }    
    172 
    173     /**
    174      * <p>
    175      * Sets the selection to the radio button whose identifier is passed in
    176      * parameter. Using -1 as the selection identifier clears the selection;
    177      * such an operation is equivalent to invoking {@link #clearCheck()}.
    178      * </p>
    179      * 
    180      * @param id
    181      *            the unique id of the radio button to select in this group
    182      * 
    183      * @see #getCheckedRadioButtonId()
    184      * @see #clearCheck()
    185      */
    186     public void check(int id) {
    187         // don't even bother
    188         if (id != -1 && (id == mCheckedId)) {
    189             return;
    190         }
    191 
    192         if (mCheckedId != -1) {
    193             setCheckedStateForView(mCheckedId, false);
    194         }
    195 
    196         if (id != -1) {
    197             setCheckedStateForView(id, true);
    198         }
    199 
    200         setCheckedId(id);
    201     }
    202 
    203     private void setCheckedId(int id) {
    204         mCheckedId = id;
    205         if (mOnCheckedChangeListener != null) {
    206             mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
    207         }
    208     }
    209 
    210     private void setCheckedStateForView(int viewId, boolean checked) {
    211         View checkedView = findViewById(viewId);
    212         if (checkedView != null && checkedView instanceof RadioButton) {
    213             ((RadioButton) checkedView).setChecked(checked);
    214         }
    215     }
    216 
    217     /**
    218      * <p>
    219      * Returns the identifier of the selected radio button in this group. Upon
    220      * empty selection, the returned value is -1.
    221      * </p>
    222      * 
    223      * @return the unique id of the selected radio button in this group
    224      * 
    225      * @see #check(int)
    226      * @see #clearCheck()
    227      */
    228     public int getCheckedRadioButtonId() {
    229         return mCheckedId;
    230     }
    231 
    232     /**
    233      * <p>
    234      * Clears the selection. When the selection is cleared, no radio button in
    235      * this group is selected and {@link #getCheckedRadioButtonId()} returns
    236      * null.
    237      * </p>
    238      * 
    239      * @see #check(int)
    240      * @see #getCheckedRadioButtonId()
    241      */
    242     public void clearCheck() {
    243         check(-1);
    244     }
    245 
    246     /**
    247      * <p>
    248      * Register a callback to be invoked when the checked radio button changes
    249      * in this group.
    250      * </p>
    251      * 
    252      * @param listener
    253      *            the callback to call on checked state change
    254      */
    255     public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
    256         mOnCheckedChangeListener = listener;
    257     }
    258 
    259     /**
    260      * {@inheritDoc}
    261      */
    262     @Override
    263     public LayoutParams generateLayoutParams(AttributeSet attrs) {
    264         return new FlowRadioGroup.LayoutParams(getContext(), attrs);
    265     }
    266 
    267     /**
    268      * {@inheritDoc}
    269      */
    270     @Override
    271     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    272         return p instanceof FlowRadioGroup.LayoutParams;
    273     }
    274 
    275     @Override
    276     protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
    277         return new LayoutParams(LayoutParams.WRAP_CONTENT,
    278                 LayoutParams.WRAP_CONTENT);
    279     }
    280 
    281     /**
    282      * <p>
    283      * This set of layout parameters defaults the width and the height of the
    284      * children to {@link #WRAP_CONTENT} when they are not specified in the XML
    285      * file. Otherwise, this class ussed the value read from the XML file.
    286      * </p>
    287      * 
    288      * <p>
    289      * See {@link android.R.styleable#LinearLayout_Layout LinearLayout
    290      * Attributes} for a list of all child view attributes that this class
    291      * supports.
    292      * </p>
    293      * 
    294      */
    295     public static class LayoutParams extends LinearLayout.LayoutParams {
    296         /**
    297          * {@inheritDoc}
    298          */
    299         public LayoutParams(Context c, AttributeSet attrs) {
    300             super(c, attrs);
    301         }
    302 
    303         /**
    304          * {@inheritDoc}
    305          */
    306         public LayoutParams(int w, int h) {
    307             super(w, h);
    308         }
    309 
    310         /**
    311          * {@inheritDoc}
    312          */
    313         public LayoutParams(int w, int h, float initWeight) {
    314             super(w, h, initWeight);
    315         }
    316 
    317         /**
    318          * {@inheritDoc}
    319          */
    320         public LayoutParams(ViewGroup.LayoutParams p) {
    321             super(p);
    322         }
    323 
    324         /**
    325          * {@inheritDoc}
    326          */
    327         public LayoutParams(MarginLayoutParams source) {
    328             super(source);
    329         }
    330 
    331         /**
    332          * <p>
    333          * Fixes the child's width to
    334          * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the
    335          * child's height to
    336          * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} when not
    337          * specified in the XML file.
    338          * </p>
    339          * 
    340          * @param a
    341          *            the styled attributes set
    342          * @param widthAttr
    343          *            the width attribute to fetch
    344          * @param heightAttr
    345          *            the height attribute to fetch
    346          */
    347         @Override
    348         protected void setBaseAttributes(TypedArray a, int widthAttr,
    349                 int heightAttr) {
    350 
    351             if (a.hasValue(widthAttr)) {
    352                 width = a.getLayoutDimension(widthAttr, "layout_width");
    353             } else {
    354                 width = WRAP_CONTENT;
    355             }
    356 
    357             if (a.hasValue(heightAttr)) {
    358                 height = a.getLayoutDimension(heightAttr, "layout_height");
    359             } else {
    360                 height = WRAP_CONTENT;
    361             }
    362         }
    363     }
    364 
    365     /**
    366      * <p>
    367      * Interface definition for a callback to be invoked when the checked radio
    368      * button changed in this group.
    369      * </p>
    370      */
    371     public interface OnCheckedChangeListener {
    372         /**
    373          * <p>
    374          * Called when the checked radio button has changed. When the selection
    375          * is cleared, checkedId is -1.
    376          * </p>
    377          * 
    378          * @param group
    379          *            the group in which the checked radio button has changed
    380          * @param checkedId
    381          *            the unique identifier of the newly checked radio button
    382          */
    383         public void onCheckedChanged(FlowRadioGroup group, int checkedId);
    384     }
    385 
    386     private class CheckedStateTracker implements
    387             CompoundButton.OnCheckedChangeListener {
    388         public void onCheckedChanged(CompoundButton buttonView,
    389                 boolean isChecked) {
    390             // prevents from infinite recursion
    391             if (mProtectFromCheckedChange) {
    392                 return;
    393             }
    394 
    395             mProtectFromCheckedChange = true;
    396             if (mCheckedId != -1) {
    397                 setCheckedStateForView(mCheckedId, false);
    398             }
    399             mProtectFromCheckedChange = false;
    400 
    401             int id = buttonView.getId();
    402             setCheckedId(id);
    403         }
    404     }
    405 
    406     /**
    407      * <p>
    408      * A pass-through listener acts upon the events and dispatches them to
    409      * another listener. This allows the table layout to set its own internal
    410      * hierarchy change listener without preventing the user to setup his.
    411      * </p>
    412      */
    413     private class PassThroughHierarchyChangeListener implements
    414             ViewGroup.OnHierarchyChangeListener {
    415         private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
    416 
    417         public void onChildViewAdded(View parent, View child) {
    418             if (parent == FlowRadioGroup.this && child instanceof RadioButton) {
    419                 int id = child.getId();
    420                 // generates an id if it's missing
    421                 if (id == View.NO_ID) {
    422                     id = child.hashCode();
    423                     child.setId(id);
    424                 }
    425                 ((RadioButton) child)
    426                         .setOnCheckedChangeListener(mChildOnCheckedChangeListener);
    427             } else if (parent == FlowRadioGroup.this
    428                     && child instanceof ViewGroup) {// 如果是复合控件
    429                 // 查找并设置id
    430                 setCheckedId((ViewGroup) child);
    431             }
    432 
    433             if (mOnHierarchyChangeListener != null) {
    434                 mOnHierarchyChangeListener.onChildViewAdded(parent, child);
    435             }
    436         }
    437 
    438         public void onChildViewRemoved(View parent, View child) {
    439             if (parent == FlowRadioGroup.this && child instanceof RadioButton) {
    440                 ((RadioButton) child).setOnCheckedChangeListener(null);
    441             } else if (parent == FlowRadioGroup.this
    442                     && child instanceof ViewGroup) {
    443                 findRadioButton((ViewGroup) child).setOnCheckedChangeListener(
    444                         null);
    445             }
    446             if (mOnHierarchyChangeListener != null) {
    447                 mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
    448             }
    449         }
    450     }
    451 }
    View Code

    简单讲解下我的实现:

    1)在addview方法中,加上判断,当前子控件是否为viewgroup类型

    @Override
        public void addView(View child, int index, ViewGroup.LayoutParams params) {
            if (child instanceof RadioButton) {
                final RadioButton button = (RadioButton) child;
                radioButtons.add(button);//将找到的控件添加到集合中
    
                if (button.isChecked()) {
                    mProtectFromCheckedChange = true;
                    if (mCheckedId != -1) {
                        setCheckedStateForView(mCheckedId, false);
                    }
                    mProtectFromCheckedChange = false;
                    setCheckedId(button.getId());
                }
            } else if (child instanceof ViewGroup) {// 如果是复合控件
                // 遍历复合控件
                ViewGroup vg = ((ViewGroup) child);
                setCheckedView(vg);
            }
    
            super.addView(child, index, params);
        }
    
        /** 查找复合控件并设置radiobutton */
        private void setCheckedView(ViewGroup vg) {
            int len = vg.getChildCount();
            for (int i = 0; i < len; i++) {
                if (vg.getChildAt(i) instanceof RadioButton) {// 如果找到了,就设置check状态
                    final RadioButton button = (RadioButton) vg.getChildAt(i);
                    // 添加到容器
                    radioButtons.add(button);
                    if (button.isChecked()) {
                        mProtectFromCheckedChange = true;
                        if (mCheckedId != -1) {
                            setCheckedStateForView(mCheckedId, false);
                        }
                        mProtectFromCheckedChange = false;
                        setCheckedId(button.getId());
                    }
                } else if (vg.getChildAt(i) instanceof ViewGroup) {// 迭代查找并设置
                    ViewGroup childVg = (ViewGroup) vg.getChildAt(i);
                    setCheckedView(childVg);
                }
            }
        }

    2)定义一个数组存放当前所有查到到的radiobutton;

    3)在onChildViewAdded方法中,判断新添加的子控件是否为viewgroup类型

    else if (parent == FlowRadioGroup.this
    					&& child instanceof ViewGroup) {// 如果是复合控件
    				// 查找并设置id
    				setCheckedId((ViewGroup) child);
    			}
    

      

    /** 查找复合控件并设置id */
        private void setCheckedId(ViewGroup vg) {
            int len = vg.getChildCount();
            for (int i = 0; i < len; i++) {
                if (vg.getChildAt(i) instanceof RadioButton) {// 如果找到了,就设置check状态
                    final RadioButton button = (RadioButton) vg.getChildAt(i);
                    int id = button.getId();
                    // generates an id if it's missing
                    if (id == View.NO_ID) {
                        id = button.hashCode();
                        button.setId(id);
                    }
                    button.setOnCheckedChangeListener(mChildOnCheckedChangeListener);
                } else if (vg.getChildAt(i) instanceof ViewGroup) {// 迭代查找并设置
                    ViewGroup childVg = (ViewGroup) vg.getChildAt(i);
                    setCheckedId(childVg);
                }
            }
        }

    下面是DivisionEditText的源码;

      1 package com.newgame.sdk.view;
      2 
      3 import android.content.Context;
      4 import android.text.Editable;
      5 import android.text.TextWatcher;
      6 import android.util.AttributeSet;
      7 import android.view.View;
      8 import android.widget.EditText;
      9 
     10 /**
     11  * 分割输入框
     12  * 
     13  * @author Administrator
     14  * 
     15  */
     16 public class DivisionEditText extends EditText {
     17 
     18     /* 每组的长度 */
     19     private Integer eachLength = 4;
     20     /* 分隔符 */
     21     private String delimiter = " ";
     22 
     23     private String text = "";
     24 
     25     public DivisionEditText(Context context) {
     26         super(context);
     27         init();
     28     }
     29 
     30     public DivisionEditText(Context context, AttributeSet attrs) {
     31         super(context, attrs);
     32         init();
     33 
     34     }
     35 
     36     public DivisionEditText(Context context, AttributeSet attrs, int defStyle) {
     37         super(context, attrs, defStyle);
     38         init();
     39     }
     40 
     41     /**
     42      * 初始化
     43      */
     44     public void init() {
     45 
     46         // 内容变化监听
     47         this.addTextChangedListener(new DivisionTextWatcher());
     48         // 获取焦点监听
     49         this.setOnFocusChangeListener(new DivisionFocusChangeListener());
     50     }
     51 
     52     /**
     53      * 文本监听
     54      * 
     55      * @author Administrator
     56      * 
     57      */
     58     private class DivisionTextWatcher implements TextWatcher {
     59 
     60         @Override
     61         public void afterTextChanged(Editable s) {
     62         }
     63 
     64         @Override
     65         public void beforeTextChanged(CharSequence s, int start, int count,
     66                 int after) {
     67         }
     68 
     69         @Override
     70         public void onTextChanged(CharSequence s, int start, int before,
     71                 int count) {
     72             // 统计个数
     73             int len = s.length();
     74             if (len < eachLength)// 长度小于要求的数
     75                 return;
     76             if (count > 1) {
     77                 return;
     78             }
     79             // 如果包含空格,就清除
     80             char[] chars = s.toString().replace(" ", "").toCharArray();
     81             len = chars.length;
     82             // 每4个分组,加上空格组合成新的字符串
     83             StringBuffer sb = new StringBuffer();
     84             for (int i = 0; i < len; i++) {
     85                 if (i % eachLength == 0 && i != 0)// 每次遍历到4的倍数,就添加一个空格
     86                 {
     87                     sb.append(" ");
     88                     sb.append(chars[i]);// 添加字符
     89                 } else {
     90                     sb.append(chars[i]);// 添加字符
     91                 }
     92             }
     93             // 设置新的字符到文本
     94             // System.out.println("*************" + sb.toString());
     95             text = sb.toString();
     96             setText(text);
     97             setSelection(text.length());
     98         }
     99     }
    100 
    101     /**
    102      * 获取焦点监听
    103      * 
    104      * @author Administrator
    105      * 
    106      */
    107     private class DivisionFocusChangeListener implements OnFocusChangeListener {
    108 
    109         @Override
    110         public void onFocusChange(View v, boolean hasFocus) {
    111             if (hasFocus) {
    112                 // 设置焦点
    113                 setSelection(getText().toString().length());
    114             }
    115         }
    116     }
    117 
    118     /** 得到每组个数 */
    119     public Integer getEachLength() {
    120         return eachLength;
    121     }
    122 
    123     /** 设置每组个数 */
    124     public void setEachLength(Integer eachLength) {
    125         this.eachLength = eachLength;
    126     }
    127 
    128     /** 得到间隔符 */
    129     public String getDelimiter() {
    130         return delimiter;
    131     }
    132 
    133     /** 设置间隔符 */
    134     public void setDelimiter(String delimiter) {
    135         this.delimiter = delimiter;
    136     }
    137 
    138 }
    View Code

    上面代码实现逻辑:在TextWatcher的onTextChanged方法中判断当前输入的字符,然后没4位添加一个空格,组成新的字符

    @Override
            public void onTextChanged(CharSequence s, int start, int before,
                    int count) {
                // 统计个数
                int len = s.length();
                if (len < eachLength)// 长度小于要求的数
                    return;
                if (count > 1) {// 设置新字符串的时候,直接返回
                    return;
                }
                // 如果包含空格,就清除
                char[] chars = s.toString().replace(" ", "").toCharArray();
                len = chars.length;
                // 每4个分组,加上空格组合成新的字符串
                StringBuffer sb = new StringBuffer();
                for (int i = 0; i < len; i++) {
                    if (i % eachLength == 0 && i != 0)// 每次遍历到4的倍数,就添加一个空格
                    {
                        sb.append(" ");
                        sb.append(chars[i]);// 添加字符
                    } else {
                        sb.append(chars[i]);// 添加字符
                    }
                }
                // 设置新的字符到文本
                // System.out.println("*************" + sb.toString());
                text = sb.toString();
                setText(text);
                setSelection(text.length());
            }

    还有其他两个自定义控件也在项目中,这里界面没体现出来,我已经放在项目中了;

    欢迎大家找出代码中的存在bug!!!!

    最后附上代码下载地址:http://www.eoeandroid.com/forum.php?mod=attachment&aid=MTIwMDM1fDM5NTYzZjQ3fDEzOTY0Mjc4NDF8NzU4MzI1fDMyODQyNw%3D%3D

  • 相关阅读:
    编译安装libusb操作流水账
    SM总线控制器没有驱动 关机后又自动重启
    mtd-utils编译安装过程
    STM32开发/烧录/调试环境搭建 基于:Win10+STM32Cube+openocd+cmsis-dap(dap-link)
    EchoServer和EchoClient模型的改进1之多线程
    CountDownLatch分析
    linux-java环境安装以及ssh
    ServerSocket01
    使用SecureCRT工具部署项目
    javaScript笔记01
  • 原文地址:https://www.cnblogs.com/Jaylong/p/3641027.html
Copyright © 2020-2023  润新知