«

Android 侧滑缩放菜单(HorizontalScrollView简单实现)

时间:2024-3-2 17:33     作者:韩俊     分类: Android


参考鸿洋博客的一篇文章:
http://blog.csdn.net/lmj623565791/article/details/39257409

最终效果图

一、菜单布局

采用列表视图ListView

left_menu.xml

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:entries="@array/color"
    android:paddingTop="5dp" 
    android:layout_marginTop="80dp">
</ListView>

其中android:entries用于通过数组资源为ListView指定列表项(也可以在代码中通过Adapter来为ListView指定要显示的列表项)

valuesarrays.xml

<resources>
    <string-array name="color">
        <item>红</item>
        <item>橙</item>
        <item>黄</item>
        <item>绿</item>
    </string-array>
</resources>

二、主布局

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"            
    xmlns:my="http://schemas.android.com/apk/res/com.example.slidemenutest"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

<!-- 自定义View,等下会定义MyView类来实现 -->
    <com.example.slidemenutest.MyView
        android:id="@+id/menu"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/menu_background"
        my:rightPadding="150dp" >

        <!-- 自定义View里面只允许一个控件,所以要嵌套 -->

        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:orientation="horizontal" >

            <!-- 导入刚才的菜单布局 -->
            <include layout="@layout/left_menu" />

            <LinearLayout
                android:layout_width="fill_parent"
                android:layout_height="fill_parent"
                android:background="@drawable/background"
                android:orientation="vertical" >

                <!-- 除了手势滑动外,还可以点击按钮弹出菜单 -->
                <Button
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:background="#80000070"
                    android:onClick="toogle"
                    android:text="菜单"
                    android:textColor="@android:color/white" />
            </LinearLayout>
        </LinearLayout>
    </com.example.slidemenutest.MyView>

</RelativeLayout>

自定义View布局里我们使用了一个自定义属性my:rightPadding=”150dp”,代表菜单弹出后跟屏幕右边界的距离,即除了菜单界面剩下的宽度。my是自己定义的命名空间标识 xmlns:my=”http://schemas.android.com/apk/res/com.example.slidemenutest” ,最后一个分隔符后面的是应用程序包名

三、自定义属性

valuesattr.xml

<resources>
    <attr name="rightPadding" format="dimension" />

    <declare-styleable name="MyView">
        <attr name="rightPadding"/>
    </declare-styleable>
</resources>

四、自定义View –MyView类实现

MyView继承HorizontalScrollView

public class MyView extends HorizontalScrollView {
    private LinearLayout layout;
    private ViewGroup mMenu;        // 菜单布局
    private ViewGroup mContent;     // 内容布局

    private int mScreenWidth;       // 屏幕宽度
    private int mMenuWidth;         // 菜单宽度
    private int mRightPadding;      // 菜单右边距

    private boolean once;           // 子view宽高初始化标识
    private boolean isOpen;         // 菜单打开标识

    // 代码中使用 new MyView()时会调用此方法
    public MyView(Context context) {
            this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);    // 调用我们需要的第三个构造方法
    }

    /**
     * 当使用了自定义属性时,会调用该构造方法
     */
    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // 通过WindowManager获取屏幕相关信息
        WindowManager wm = (WindowManager) context
                .getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        mScreenWidth = outMetrics.widthPixels;      // 获取屏幕宽度

        Log.v("TAG","density:"+outMetrics.density);
        Log.v("TAG","densityDpi:"+outMetrics.densityDpi);

         // 通过TypedArray获取自定义的属性
         TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
                 R.styleable.MyView, defStyleAttr, 0);
         // 通过遍历自定义属性找到我们需要的rightPadding属性
         for(int i = 0 ;i<a.getIndexCount();i++){
             // attr为属性名
             int attr = a.getIndex(i);
             switch (attr) {
             case R.styleable.MyView_rightPadding:
                 // 如果没有设置右边距,则定义一个默认值,用TypedValue.applyDimension函数把50dp值转为像素值
                 // 不同设备的density不同 ,dp和px的转换也不同 。 如果density为1.5,则1dp = 1.5px
                 int defValue = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, 
                         context.getResources().getDisplayMetrics());

                 // 获取属性值转为像素返回。如果属性没有设置值,则返回设置的默认值
                 mRightPadding =  a.getDimensionPixelSize(attr, defValue);
                 break;

             default:
                 break;
             }
         }
         a.recycle();           // 使用完后记得要释放掉
    }

    /**
     * 系统显示布局前先要为每个子view设置宽高,然后再设置它们的摆放位置
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // onMeasure方法会被系统多次调用,这里设置只进行一次初始化
        if (!once) {
            layout = (LinearLayout) getChildAt(0);      // 自定义View里的第一个view,还记得我们用LinearLayout嵌套其它子view吗?
            mMenu = (ViewGroup) layout.getChildAt(0);   // LinearLayout里的第一个view是菜单
            mContent = (ViewGroup) layout.getChildAt(1);// LinearLayout里的第二个view是内容

            // 设置菜单宽度 = 屏幕宽度-我们设置的右边距 (剩下的宽度还是要显示一点内容的~)
            mMenuWidth = mMenu.getLayoutParams().width = mScreenWidth - mRightPadding;  
            // 内容宽度当然是整个屏幕宽啦
            mContent.getLayoutParams().width = mScreenWidth;

            Log.v("TAG","mScreenWidth:"+mScreenWidth);
            Log.v("TAG","mRightPadding:"+mRightPadding);
            Log.v("TAG","mMenuWidth:"+mMenuWidth);
            once = true;            // 第二次系统调用onMeasure方法后就不会再重复初始化了
        }

    }

    /**
     * 设置了子view的宽高后,自然就是它们自己选择在屏幕中的摆放位置啦
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        // onLayout也会多次调用
        if(changed){
            this.scrollTo(mMenuWidth, 0);   // 隐藏菜单。 虽然只有简单的一句代码,但如果不知道原理的话是很难理解的
            /*
             * 滚动视图有一个固定的坐标系,这个坐标系的原点在一开始它显示到屏幕中的左上角的位置。原点往左是视图的负坐标,原点往右是视图的正坐标
             * 在这里一开始先显示的是菜单视图,所以菜单的左上角为这个滚动视图的原点,菜单和内容的边界 x坐标值=菜单的宽度
             * scrollTo(x,0)是把视图坐标系的(x,0)位置移动到屏幕左边界(见示意图)
             */
            isOpen = false;         // 菜单关闭状态

        }

    }

假设屏幕宽度400,菜单宽度300,右边距100

没有移动视图之前,即没有调用this.scrollToX(mMenuWidth,0); 之前,视图显示如下第一个图,因为是水平顺序排列,所以先显示菜单。此时就决定了滚动视图的坐标:菜单左上角为视图原点,且这视图坐标不会再变。

调用了this.scrollToX(mMenuWidth,0); 之后,视图坐标(300,0)移动到屏幕坐标系(0,0)(即左上角),这样菜单就隐藏了。

菜单拖动过程中,左边界所在滚动视图位置 x 坐标值不断变化,从300变化到0

    /*
     * 通过判断手势实现 滑动多少距离才执行 打开/隐藏 菜单
     */
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
        case MotionEvent.ACTION_UP:
            // 当手指拖动后松开判断此时屏幕左边界位置所在的x坐标值  来 决定显示还是隐藏
            int scrollx = getScrollX();     // getScrollX()就是此时屏幕左边界所在的视图的x坐标值,即getScrollX() == x

            // 左边界位置的x值大于菜单宽度的一半,即未显示的菜单宽度大于显示的菜单宽度,此时应该隐藏
            if(scrollx >= mMenuWidth/2){
                this.scrollTo(mMenuWidth, 0);
                isOpen=false;
                return true;
            }

            // 左边界位置的x值小于菜单宽度的一半,即显示的菜单宽度大于未显示的菜单宽度,此时应该显示
            else{
                // 把视图原点(即菜单左上角)移动至屏幕原点(左上角)
                this.scrollTo(0, 0);
                isOpen = true;
                return true;
            }
        default:
            break;
        }
        return super.onTouchEvent(ev);
    }
    // 打开菜单
    private void openMenu(){
        if(isOpen)
            return;  // 如果菜单已经打开则返回
        this.smoothScrollTo(0, 0);
        isOpen=true;
    }

    // 关闭菜单
    private void closeMenu(){
        if(!isOpen)
            return;  // 如果菜单已经关闭则返回
        this.smoothScrollTo(mMenuWidth, 0);
        isOpen = false;
    }

    // 点击按钮后会调用切换方法
    public void toogle(){
        if(isOpen)
            closeMenu();
        else 
            openMenu();
    }

五、MainActivity类的实现

public class MainActivity extends Activity {
    private MyView myview;          // 自定义view类

    private ListView listView;      // 菜单

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

        myview = (MyView)findViewById(R.id.menu);
        listView = (ListView) findViewById(R.id.list);
        // 监听列表视图的点击事件
        listView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                    int position, long id) {
                toogle(view);   // 点击菜单项后切换到内容视图
                String result = parent.getItemAtPosition(position).toString();
                // 在内容视图显示消息提示框,提示框里面是被点击菜单项的文本
                Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show();
            }
        });
    }

    /*
     * 布局中为Button注册了toogle方法,注意是public
     */
    public void toogle(View view)
    {
        myview.toogle();    // 调用MyView类中的toogle方法
    }
}

普通侧滑菜单演示:


可以看到当菜单拖出不到菜单宽度的一半时 ,手指松开菜单会隐藏回去,只有超过了那个距离才能打开菜单。关闭菜单也是同理。点击按钮也会弹出菜单,点击菜单项会切换回内容视图 并显示菜单项的文本。

六、被覆盖式菜单

当拖出菜单的时候,菜单显示效果就像 菜单原来是铺在内容视图的下层,移开内容就可以看见被覆盖的菜单,移动中菜单是静止的。和上面菜单跟内容相连滑动的效果不同

实现这个效果只需要简单的一行代码,包含的信息量也很大。需要用到开源包com.nineoldandroids.view.ViewHelper。这个包主要是为了兼容3.0以下使用Animation。jar包粘贴到libs目录下即可使用

在重写的父类onScrollChanged方法中实现

    /**
     * 抽屉式滑动菜单
     * 当视图滚动时,会实时调用此方法。l实际上是屏幕左边界处的 滚动视图x坐标值
     * 原理:在滑动过程中,保持菜单视图在屏幕左边界。利用开源框架nineoldandroids中的ViewHelper.setTranslationX方法,
     * 把菜单视图移动到x坐标为l处,即左边界。菜单视图的移动不影响屏幕左边界处的滚动视图x坐标值
     */
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);

        // 让菜单实时贴边.菜单打开过程中 l的变化范围是 mMenuWidth ~ 0
        ViewHelper.setTranslationX(mMenu, l );
    }

效果

七、缩放效果菜单

在菜单拖出的过程中,菜单从缩放后的大小逐渐恢复成正常大小,从半透明到不透明,同时 内容视图从正常大小缩小到一定比例

同样是在onScrollChanged中实现

@Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);

        // 让菜单实时贴边
        ViewHelper.setTranslationX(mMenu, l * 0.8f );       // l*0.8是让菜单项文本跟屏幕左边界尽量贴边

        /*
         * 利用实时变化的比率来达到实时缩放效果
         */

        // 在拉出过程中,l从 255 ~ 0
        float rate = l * 1.0f / mMenuWidth;                 // rate 从 1.0 ~ 0.0    
        float menuScale = 1.0f - 0.3f * rate;               // 菜单缩放效果从 0.7 ~ 1.0
        float menuAlpha = menuScale;                            // 菜单透明度从 0.7 ~ 1.0
        float contentScale = 1.0f - 0.3f * ( 1 - rate );        // 内容缩放效果从 1.0 ~ 0.7

        // 菜单缩放
        ViewHelper.setScaleX(mMenu, menuScale);
        ViewHelper.setScaleY(mMenu, menuScale);
        ViewHelper.setAlpha(mMenu, menuAlpha);

        // 内容缩放
        ViewHelper.setScaleX(mContent, contentScale);
        ViewHelper.setScaleY(mContent, contentScale);
    }

效果(即文章最开始的图):

八、总结

继承HorizontalScrollView实现自定义View,并设置自定义属性,然后在主布局中引入。之后利用scrollToX移动视图,把菜单隐藏。接着监听手势判断执行菜单的关闭和打开。通过开源包中的ViewHelper.setTranslationX实现菜单实时贴紧屏幕左边界,利用边界值的变化设置缩放比例,实现菜单和内容的缩放。

HorizontalScrollView实现滑动菜单比较简单,且整个项目我已经按照我的理解详细注释,配合示意图,应该不难理解。

最后,奉上项目包,容各位慢慢体会

点击下载

标签: android

热门推荐