最近开发需求中要模仿微信朋友圈文章的展开收起功能,网上找了找,发现都有问题,于是乎自己在前辈的基础上进行了一定量的修改,下边将源码贴出来供大家参考:
1.主Activity布局文件就不粘贴了,很简单,就一个ListView.
2.主Activity功能实现:

  1. <font face="宋体" size="3">package com.example.textviewdemo;
  2. import java.util.HashMap;
  3. import android.app.Activity;
  4. import android.content.Context;
  5. import android.os.Bundle;
  6. import android.view.LayoutInflater;
  7. import android.view.View;
  8. import android.view.ViewGroup;
  9. import android.widget.BaseAdapter;
  10. import android.widget.ListView;
  11. import android.widget.TextView;
  12. import android.widget.TextView.BufferType;
  13. public class MainActivity extends Activity {
  14. String mStr;
  15. int type;
  16. @Override
  17. protected void onCreate(Bundle savedInstanceState) {
  18. super.onCreate(savedInstanceState);
  19. setContentView(R.layout.activity_main);
  20. Globl.map = new HashMap<Integer, Boolean>();
  21. ListView listview = (ListView) findViewById(R.id.listview);
  22. mStr = "手指在ListView上下滚动时,ListViewItem背景变黑,因为在滚动的时候为了提升性能做了优化,为提高滚动的性能,Android 框    架在ListView中引入CacheColorHint属性。如果该值为非0,则说明该ListView绘制在单色不透明的背景上,在默认情况下该值 为        #191919,也就是黑色主题中的黑色背景颜色值,这样当ListView滚动的时候";
  23. listview.setAdapter(new MyListAdpter(this));
  24. }
  25. class MyListAdpter extends BaseAdapter {
  26. Context con;
  27. CollapsibleTextView tv;
  28. public MyListAdpter(Context con) {
  29. this.con = con;
  30. }
  31. @Override
  32. public int getCount() {
  33. // TODO Auto-generated method stub
  34. return 10;
  35. }
  36. @Override
  37. public Object getItem(int position) {
  38. // TODO Auto-generated method stub
  39. return null;
  40. }
  41. @Override
  42. public long getItemId(int position) {
  43. // TODO Auto-generated method stub
  44. return 0;
  45. }
  46. HashMap<Integer, View> hashM = new HashMap<Integer, View>();
  47. @Override
  48. public View getView(int position, View convertView, ViewGroup parent) {
  49. Holder holder = null;
  50. View view;
  51. if (hashM.get(position) == null) {
  52. holder = new Holder();
  53. view = LayoutInflater.from(con).inflate(R.layout.item_list, null);
  54. holder.tv = (CollapsibleTextView) view.findViewById(R.id.tv_text);
  55. holder.tvcount = (TextView) view.findViewById(R.id.tvcount);
  56. view.setTag(holder);
  57. hashM.put(position, view);
  58. } else {
  59. view = hashM.get(position);
  60. holder = (Holder) view.getTag();
  61. }
  62. // if (Globl.map.get(position) == false) {
  63. // Globl.map.put(position, false);
  64. // type = 2;
  65. // } else {
  66. // type = 1;
  67. // }
  68. // tv.setNowType(type);
  69. // int typeNow = tv.getNowType();
  70. holder.tvcount.setText(position + "");
  71. holder.tv.setDesc(mStr, holder.tv, BufferType.NORMAL);
  72. return view;
  73. }
  74. class Holder {
  75. CollapsibleTextView tv;
  76. TextView tvcount;
  77. }
  78. }
  79. }

3.自定义控件CollapsibleTextView 源码:

  1. <font face="宋体" size="3">/**
  2. * @Explain: Text过长收起 带有查看全文/收起功能控件;
  3. * @Author:LYl
  4. * @Time:2014-11-27 下午4:33:05
  5. * @Version V2.1.54
  6. */
  7. public class CollapsibleTextView extends LinearLayout implements
  8. OnClickListener {
  9. /** 最大显示的行数 */
  10. private static final int DEFAULT_MAX_LINE_COUNT = 8;
  11. /** 实际展示的行数 */
  12. private static final int DEFAULT_SHOW_LINE_COUNT = 6;
  13. private static final int COLLAPSIBLE_STATE_NONE = 0;
  14. /** View处于展开状态 **/
  15. private static final int COLLAPSIBLE_STATE_SHRINKUP = 1;
  16. /** view收缩时状态 **/
  17. private static final int COLLAPSIBLE_STATE_SPREAD = 2;
  18. /** 显示内容的View */
  19. private TextView tv_context;
  20. /** 展开/收起按钮 */
  21. private TextView bt_spread;
  22. private String shrinkup;
  23. private String spread;
  24. /** 当前正处于的状态 */
  25. // private int mState;
  26. private boolean flag;
  27. private int nowType;
  28. private CollapsibleTextView coTextView;
  29. /** 判断是不是点击了查看更多、收起 */
  30. private boolean isClicke = false;
  31. private int lookCount = 0;
  32. public CollapsibleTextView(Context context, AttributeSet attrs) {
  33. super(context, attrs);
  34. shrinkup = "收起";
  35. spread = "查看全文";
  36. View view = inflate(context, R.layout.collapsible_textview, this);
  37. view.setPadding(0, -1, 0, 0);
  38. tv_context = (TextView) view.findViewById(R.id.tv_context);
  39. bt_spread = (TextView) view.findViewById(R.id.bt_spread);
  40. bt_spread.setOnClickListener(this);
  41. }
  42. public CollapsibleTextView(Context context) {
  43. this(context, null);
  44. }
  45. /**
  46. * 赋值
  47. */
  48. public final void setDesc(CharSequence charSequence,
  49. CollapsibleTextView tv, BufferType bufferType) {
  50. this.coTextView = tv;
  51. // 对内容中的网址进行处理;
  52. tv_context.setAutoLinkMask(Linkify.WEB_URLS);
  53. tv_context.setMovementMethod(LinkMovementMethod.getInstance());
  54. tv_context.setText(charSequence, bufferType);
  55. // 初始类型
  56. if (lookCount == 0) {
  57. coTextView.setNowType(COLLAPSIBLE_STATE_SPREAD);
  58. }
  59. lookCount += 1;
  60. // TODO LYL 放到ListView中需要加下句:falg=false;一般情况去掉就可
  61. flag = false;
  62. requestLayout();
  63. }
  64. @Override
  65. public void onClick(View v) {
  66. flag = false;
  67. isClicke = true;
  68. requestLayout();
  69. }
  70. @Override
  71. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  72. super.onLayout(changed, l, t, r, b);
  73. if (!flag) {
  74. flag = true;
  75. if (tv_context.getLineCount() <= DEFAULT_MAX_LINE_COUNT) {
  76. bt_spread.setVisibility(View.GONE);
  77. tv_context.setMaxLines(DEFAULT_MAX_LINE_COUNT + 1);
  78. coTextView.setNowType(COLLAPSIBLE_STATE_NONE);
  79. } else {
  80. post(new InnerRunnable());
  81. }
  82. }
  83. }
  84. class InnerRunnable implements Runnable {
  85. @Override
  86. public void run() {
  87. int zType = 0;
  88. // 第一次进入操作(没有点击并且是第一次进入);
  89. System.out.println("lookCount:" + lookCount);
  90. if (!isClicke && lookCount == 1) {
  91. if (coTextView.getNowType() == COLLAPSIBLE_STATE_SPREAD) {
  92. tv_context.setMaxLines(DEFAULT_SHOW_LINE_COUNT);
  93. bt_spread.setVisibility(View.VISIBLE);
  94. bt_spread.setText(spread);
  95. zType = COLLAPSIBLE_STATE_SHRINKUP;
  96. } else if (coTextView.getNowType() == COLLAPSIBLE_STATE_SHRINKUP) {
  97. tv_context.setMaxLines(Integer.MAX_VALUE);
  98. bt_spread.setVisibility(View.VISIBLE);
  99. bt_spread.setText(shrinkup);
  100. zType = COLLAPSIBLE_STATE_SPREAD;
  101. }
  102. coTextView.setNowType(zType);
  103. // 点击了查看更多、收起转换状态;
  104. } else if (isClicke) {
  105. isClicke = false;
  106. if (coTextView.getNowType() == COLLAPSIBLE_STATE_SPREAD) {
  107. tv_context.setMaxLines(DEFAULT_SHOW_LINE_COUNT);
  108. bt_spread.setVisibility(View.VISIBLE);
  109. bt_spread.setText(spread);
  110. coTextView.setNowType(COLLAPSIBLE_STATE_SHRINKUP);
  111. } else if (coTextView.getNowType() == COLLAPSIBLE_STATE_SHRINKUP) {
  112. tv_context.setMaxLines(Integer.MAX_VALUE);
  113. bt_spread.setVisibility(View.VISIBLE);
  114. bt_spread.setText(shrinkup);
  115. coTextView.setNowType(COLLAPSIBLE_STATE_SPREAD);
  116. }
  117. // 滑动listView 从新载入到可见界面 不做操作,保持原有状态;(为了后面看得人能够更理解写上)
  118. } else if (!isClicke && lookCount != 1) {
  119. }
  120. }
  121. }
  122. public int getNowType() {
  123. return nowType;
  124. }
  125. public void setNowType(int nowType) {
  126. this.nowType = nowType;
  127. }
  128. }

public class CollapsiblePanel extends LinearLayout {
    /**
     * TAG
     */
    private static final String TAG = "CollapsiblePanel";
    /**
     * DEBUG
     */
    private static final boolean DEBUG = BuildConfig.DEBUG;
    /**
     * 内容View
     */
    private View mContentView;
    /**
     * 可收缩的View
     */
    private View mCollapsibleView;
    /**
     * 可收缩的大小
     */
    private int mCollapsibleSize;
    /**
     * 收缩的监听器
     */
    private OnCollapsibleListener mCollapsibleListener;
    /**
     * 收缩动画的时间
     */
    private int mAnimDuration = 0;  //SUPPRESS CHECKSTYLE
    /**
     * 判断当前可收缩View是否是打开状态
     */
    private boolean mIsOpened = false;
    /**
     * 可收缩View默认是否可见
     */
    private boolean mCollapsibleViewDefaultVisible = false;
    /**
     * Toggle是否可用
     */
    private boolean mToggleEnable = true;
    /**
     * 不使用Alpha动画
     */
    private boolean mWithoutAlphaAnim = true;
    /**
     * 收缩是否有动画效果
     */
    private boolean mDoAnimation = true;

public interface OnCollapsibleListener {
        /**
         * 动画结束监听
         *
         * @param isOpened 当前的stretchView是否是打开的,
         */
        void onCollapsibleFinished(boolean isOpened);

/**
         * 动画过程中使用Transformation的接口
         *
         * @param from             from
         * @param to               to
         * @param interpolatedTime interpolatedTime
         */
        void applyTransformation(int from, int to, float interpolatedTime);
    }

/**
     * 构造方法
     *
     * @param context context
     */
    public CollapsiblePanel(Context context) {
        super(context);
        init(context, null);
    }

/**
     * 构造方法
     *
     * @param context context
     * @param attrs   attrs
     */
    public CollapsiblePanel(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

/**
     * 构造方法
     *
     * @param context  context
     * @param attrs    attrs
     * @param defStyle defStyle
     */
    @SuppressLint("NewApi")
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public CollapsiblePanel(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs);
    }

/**
     * 初始化
     *
     * @param context context
     * @param attrs   attrs
     */
    private void init(Context context, AttributeSet attrs) {
        setOrientation(LinearLayout.VERTICAL);
        mAnimDuration = 280;
    }

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mCollapsibleSize == 0 && mCollapsibleView != null) {
            mCollapsibleView.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);

if (LinearLayout.VERTICAL == getOrientation()) {
                mCollapsibleSize = mCollapsibleView.getMeasuredHeight();
                if (!mCollapsibleViewDefaultVisible) {
                    mCollapsibleView.getLayoutParams().height = 0;
                }
            } else {
                mCollapsibleSize = mCollapsibleView.getMeasuredWidth();
                if (!mCollapsibleViewDefaultVisible) {
                    mCollapsibleView.getLayoutParams().width = 0;
                }
            }
        }

super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

/**
     * 设置控件初始时是否可见
     *
     * @param visible 是否可见
     */
    public void setCollapsibleViewDefaultVisible(boolean visible) {
        mCollapsibleViewDefaultVisible = visible;
        // 默认可见的话,则认为是展开的
        mIsOpened = visible;
    }

/**
     * 控件初始时是否可见
     *
     * @return visible
     */
    public boolean getCollapsibleViewDefaultVisible() {
        return mCollapsibleViewDefaultVisible;
    }

/**
     * 设置toggle是否可用,如果设置为false,则{@link #toggle()}接口无效
     *
     * @param enable enable
     */
    public void setToggleEnable(boolean enable) {
        mToggleEnable = enable;
    }

/**
     * 获取当前的主View
     *
     * @return view
     */
    public View getContentView() {
        return mContentView;
    }

/**
     * 获取当前的扩展View
     *
     * @return view
     */
    public View getStretchView() {
        return mCollapsibleView;
    }

/**
     * 设置主View
     *
     * @param view view
     */
    public void setContentView(View view) {
        if (view != null) {
            if (mContentView != null) {
                removeView(this.mContentView);
            }

mContentView = view;
            addView(mContentView, 0);
        }
    }

/**
     * 设置收缩的View
     *
     * @param collapsibleView 可以收缩的View
     */
    public void setCollapsibleView(View collapsibleView) {
        if (collapsibleView != null) {
            if (mCollapsibleView != null) {
                removeView(mCollapsibleView);
                // 在重新设置时,将该值置为0,否则新view将不能显示正确的高度
                mCollapsibleSize = 0;
            }

mCollapsibleView = collapsibleView;
            addView(mCollapsibleView);
        }
    }

/**
     * 得到可收缩View的大小
     *
     * @return 可收缩View的大小
     */
    public int getCollapsibleSize() {
        return mCollapsibleSize;
    }

/**
     * 设置收缩的监听
     *
     * @param listener listener
     */
    public void setOnCollapsibleListener(OnCollapsibleListener listener) {
        mCollapsibleListener = listener;
    }

/**
     * 当前的视图是否已经展开
     *
     * @return true/false
     */
    public boolean isCollapsibleViewOpened() {
        return mIsOpened;
    }

/**
     * 设置展开(或者收缩)动画的时间,默认280ms
     *
     * @param durationMs durationMs
     */
    public void setCollapsibleAnimDuration(int durationMs) {
        if (durationMs >= 0) {
            mAnimDuration = durationMs;
        } else {
            throw new IllegalArgumentException("Animation duration cannot be negative");
        }
    }

/**
     * 展开/收起View
     *
     * @return true/false
     */
    public boolean toggle() {
        // 如果不允许展开
        if (!mToggleEnable) {
            return false;
        }

// 如果动画正在进行,不执行任何操作
        if (isAnimationPlaying()) {
            return false;
        }

if (mIsOpened) {
            closeCollapsibleView();
        } else {
            openCollapsibleView();
        }

return true;
    }

/**
     * 展开视图
     */
    public void openCollapsibleView() {
        if (mCollapsibleView == null) {
            return;
        }

post(new Runnable() {
            @Override
            public void run() {
                if (mDoAnimation) {
                    CollapsibleAnimation animation = new CollapsibleAnimation(0, mCollapsibleSize, 0.0f, 1.0f);
                    animation.setDuration(mAnimDuration);
                    animation.setAnimationListener(mCollapsibleAnimListener);
                    mCollapsibleView.startAnimation(animation);
                    invalidate();
                } else {
                    setCollapsibleViewSize(mCollapsibleSize);
                    onCollapsibleEnd();
                }
            }
        });
    }

/**
     * 收起视图
     */
    public void closeCollapsibleView() {
        if (mCollapsibleView == null) {
            return;
        }

post(new Runnable() {
            @Override
            public void run() {
                if (mDoAnimation) {
                    CollapsibleAnimation animation = new CollapsibleAnimation(mCollapsibleSize, 0, 1.0f, 0.0f);
                    animation.setDuration(mAnimDuration);
                    animation.setAnimationListener(mCollapsibleAnimListener);
                    mCollapsibleView.startAnimation(animation);
                    invalidate();
                } else {
                    setCollapsibleViewSize(0);
                    onCollapsibleEnd();
                }
            }
        });
    }

/**
     * 收缩View展开或收缩时调用
     *
     * @param isOpened isOpened
     */
    protected void onCollapsibleFinished(boolean isOpened) {

}

/**
     * 重置大小
     */
    protected void resetCollapsibleSize() {
        mCollapsibleSize = 0;
        requestLayout();
    }

/**
     * 设置收缩View的大小
     *
     * @param size size
     */
    private void setCollapsibleViewSize(int size) {
        if (null == mCollapsibleView) {
            return;
        }

LayoutParams params = (LayoutParams) mCollapsibleView.getLayoutParams();
        if (null != params) {
            if (LinearLayout.VERTICAL == getOrientation()) {
                params.height = size;
            } else {
                params.width = size;
            }

mCollapsibleView.setLayoutParams(params);
        }
    }

/**
     * 判断动画是否正在播放
     *
     * @return true/false
     */
    private boolean isAnimationPlaying() {
        if (null != mCollapsibleView) {
            Animation anim = mCollapsibleView.getAnimation();
            if (null != anim && !anim.hasEnded()) {
                return true;
            }
        }

return false;
    }

/**
     * 动画的监听器
     */
    private AnimationListener mCollapsibleAnimListener = new AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {
        }

@Override
        public void onAnimationRepeat(Animation animation) {
        }

@Override
        public void onAnimationEnd(Animation animation) {
            onCollapsibleEnd();
        }
    };

/**
     * 收缩结束时调用
     */
    private void onCollapsibleEnd() {
        mIsOpened = !mIsOpened;
        if (mCollapsibleListener != null) {
            mCollapsibleListener.onCollapsibleFinished(mIsOpened);
        }

if (null != mCollapsibleView) {
            mCollapsibleView.setAnimation(null);
        }

onCollapsibleFinished(mIsOpened);
    }

/**
     * 伸缩动画
     */
    private class CollapsibleAnimation extends Animation {
        /**
         * 开始的大小
         */
        private int mFromSize;
        /**
         * 结束的大小
         */
        private int mToSize;
        /**
         * 开始的Alpha
         */
        private float mFromAlpha;
        /**
         * 结束的Alpha
         */
        private float mToAlpha;

/**
         * 构造方法
         *
         * @param fromSize  初始的大小
         * @param toSize    结束的大小
         * @param fromAlpha 初始的透明度
         * @param toAlpha   结束的透明度
         */
        public CollapsibleAnimation(int fromSize, int toSize, float fromAlpha, float toAlpha) {
            mFromSize = fromSize;
            mToSize = toSize;
            mFromAlpha = fromAlpha;
            mToAlpha = toAlpha;
        }

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

@Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            if (mCollapsibleView != null) {
                if (mWithoutAlphaAnim) {
                    // Do nothing
                } else {
                    // 改变透明度
                    final float alpha = mFromAlpha;
                    t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));
                }

// 改变大小
                final int fromSize = mFromSize;
                int size = (int) (fromSize + (mToSize - fromSize) * interpolatedTime);
                setCollapsibleViewSize(size);

if (null != mCollapsibleListener) {
                    mCollapsibleListener.applyTransformation(mFromSize, mToSize, interpolatedTime);
                }
            }
        }
    }
}

Android开发之仿微信显示更多文字的View相关推荐

  1. Android编程之仿微信显示更多文字的View

    微信朋友圈中,如果好友发表的文字过长,会自动收缩起来,底下有提示,当点击"显示更多"时才会展开. 首先定义布局文件(很简单,不解释): <?xml version=" ...

  2. android 微信朋友圈 全功能,Android仿微信朋友圈文字展开全文功能 Android自定义TextView仿微信朋友圈文字展开全文功能...

    Android自定义TextView仿微信朋友圈文字信息,展开全文功能 代码及注释如下: 首先写一个xml文件 showmore.xml: android:orientation="vert ...

  3. android 微信朋友圈 全功能,Android自定义TextView仿微信朋友圈文字展开全文功能

    Android自定义TextView仿微信朋友圈文字信息,展开全文功能 代码及注释如下: 首先写一个xml文件 showmore.xml: android:orientation="vert ...

  4. Android开发之仿微信底部导航切换(Compose版本)附加源码下载

    老套路,先上(献上)效果图 实际上这个页面在Android开发中太常见了.所以学些了下Compose版本 说下核心: 1.需要记录每次切换的页面position var currentNavigati ...

  5. android开发百度地图坐标偏差,利用百度地图Android sdk高仿微信发送位置功能及遇到的问题...

    接触了百度地图开发平台半个月了,这2天试着模仿了微信给好友发送位置功能,对百度地图的操作能力又上了一个台阶 我在实现这个功能的时候,遇到一些困难,可能也是别人将会遇到的困难,特在此列出 1.在微信发送 ...

  6. android仿微信图片上传进度,Android开发之模仿微信打开网页的进度条效果(高仿)...

    一,为什么说是真正的高仿? 阐述这个问题前,先说下之前网上的,各位可以复制这段字,去百度一下  "仿微信打开网页的进度条效果",你会看到有很多类似的文章,不过他们有个共同点,就是实 ...

  7. android仿微信 进度条,Android开发之模仿微信打开网页的进度条效果(高仿)

    一,为什么说是真正的高仿? 阐述这个问题前,先说下之前网上的,各位可以复制这段字,去百度一下  "仿微信打开网页的进度条效果" ,你会看到有很多类似的文章,不过他们有个共同点,就是 ...

  8. Android 仿微信显示的聊天照片

    Android 仿微信显示的聊天照片 Android 仿微信显示的聊天照片,效果如下图所示: 这种显示的样式就是和微信的显示照片的样式是一样的,微信的实现我不知道是否和我一样,今天我来和大家介绍一下我 ...

  9. 自定义控件 仿微信朋友圈文字展开全文功能

    自定义TextView仿微信朋友圈文字信息,展开全文功能 代码及注释如下: 首先写一个xml文件 showmore.xml: <?xml version="1.0" enco ...

最新文章

  1. Python 爬虫练手项目—酒店信息爬取
  2. java中static作用详解
  3. PHP中空格占位数吗,HTML空格占位
  4. 把激光雷达放在iPad上是怎样的体验?看到“测距仪”App的效果我震惊了
  5. iOS流媒体直播整个框架介绍(HLS、RTSP)
  6. (JAVA学习笔记) static关键字详解
  7. 工程制图 ( 标准件与常用件)
  8. ionic移动开发流程api
  9. C#使用七牛云存储上传下载文件、自定义回调
  10. 2019 年总结 | 31岁,不过是另一个开始
  11. nginx+kibana代理以及简单认证
  12. Java中的基本类型和引用类型(未完)
  13. 10A 的GROUP和CUI使用
  14. java 获取上传文件后缀_java 文件上传相关知识及得到后缀名
  15. 最新kali之arping
  16. 别细看|请收藏|堆垛机故障大全及解决办法
  17. Java 1072 开学寄语
  18. 《python3网络爬虫开发实战》学习笔记:pc与安卓代理证书都安装好但是无法监听——记自己的踩坑路径
  19. jQuery中index的用法
  20. 请注意:黑客开始用云隐藏IP地址

热门文章

  1. MATLAB实现的彩色数字水印算法
  2. 织梦的网站地图怎么做html,织梦(dedecms)网站地图改变生成目录的方法
  3. serv服务器怎么设置欢迎消息,serv-u登陆欢迎信息参数设置技巧_其他工具教程
  4. 关于selenium自动化测试退出登录网易163邮箱的总结感想
  5. 企业直播营销的商业价值是什么?
  6. c语言程序设计工资纳税系统,c语言程序设计,纳税工资系统(29页)-原创力文档...
  7. vsco怎么两个滤镜叠加_别再乱套滤镜了!这4款VSCO滤镜好看到哭!
  8. MAC 开机自动连接iPad
  9. ViTag :在线 WiFi 精细时间测量辅助多人环境中的视觉-运动身份关联
  10. NGUI制作头顶血条个人小问题解决方法