学更好的别人,

做更好的自己。

——《微卡智享》

本文长度为6259,预计阅读11分钟

前言

今天是折叠屏开发的第三篇,前面已经介绍了铰链的角度监听和Jetpack Window实现监听效果,今天我们就来做个折叠状态和展开状态显示的不同效果Demo,本篇的重点主要是两个,一是布局文件的设计,另一个就是MotionLayout的动画效果。

实现效果

竖屏折叠

竖屏展开

横屏折叠

横屏展开

上图中可以看到,竖屏折叠时,宫格布局和按钮都在同一界面,按钮在下方,当竖屏展开后,宫格布局移动到左边,而按钮布局移动到右边了,并且由原来的水平排列改为了垂直排列(完整的效果视频看P2)。接下来就来看看怎么实现的。

代码实现

微卡智享

核心代码

实现分屏布局,最主要的就是靠我们自己定义的一个FrameLayout,里面内置了WindowLayoutInfo的参数,参数传入的WindowLayoutInfo来判断当前的什么状态,而应用什么样的布局(左右,上下还是合并)

首先要创建一个attr.xml

<resources><declare-styleable name="SplitLayout"><attr name="startViewId" format="reference" /><attr name="endViewId" format="reference" /></declare-styleable>
</resources>

SplitLayout的代码:

package pers.vaccae.mvidemo.ui.viewimport android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.constraintlayout.widget.ConstraintAttribute.setAttributes
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowLayoutInfo
import pers.vaccae.mvidemo.R/*** 作者:Vaccae* 邮箱:3657447@qq.com* 创建时间:15:07* 功能模块说明:*/
class SplitLayout :FrameLayout{private var windowLayoutInfo: WindowLayoutInfo? = nullprivate var startViewId = 0private var endViewId = 0private var lastWidthMeasureSpec: Int = 0private var lastHeightMeasureSpec: Int = 0constructor(context: Context) : super(context)constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {setAttributes(attrs)}constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context,attrs,defStyleAttr) {setAttributes(attrs)}private fun setAttributes(attrs: AttributeSet?) {context.theme.obtainStyledAttributes(attrs, R.styleable.SplitLayout, 0, 0).apply {try {startViewId = getResourceId(R.styleable.SplitLayout_startViewId, 0)endViewId = getResourceId(R.styleable.SplitLayout_endViewId, 0)} finally {recycle()}}}fun updateWindowLayout(windowLayoutInfo: WindowLayoutInfo) {this.windowLayoutInfo = windowLayoutInforequestLayout()}override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {val startView = findStartView()val endView = findEndView()val splitPositions = splitViewPositions(startView, endView)if (startView != null && endView != null && splitPositions != null) {val startPosition = splitPositions[0]val startWidthSpec = MeasureSpec.makeMeasureSpec(startPosition.width(),MeasureSpec.EXACTLY)val startHeightSpec = MeasureSpec.makeMeasureSpec(startPosition.height(),MeasureSpec.EXACTLY)startView.measure(startWidthSpec, startHeightSpec)startView.layout(startPosition.left, startPosition.top, startPosition.right,startPosition.bottom)val endPosition = splitPositions[1]val endWidthSpec = MeasureSpec.makeMeasureSpec(endPosition.width(), MeasureSpec.EXACTLY)val endHeightSpec = MeasureSpec.makeMeasureSpec(endPosition.height(),MeasureSpec.EXACTLY)endView.measure(endWidthSpec, endHeightSpec)endView.layout(endPosition.left, endPosition.top, endPosition.right,endPosition.bottom)} else {super.onLayout(changed, left, top, right, bottom)}}private fun findStartView(): View? {var startView = findViewById<View>(startViewId)if (startView == null && childCount > 0) {startView = getChildAt(0)}return startView}private fun findEndView(): View? {var endView = findViewById<View>(endViewId)if (endView == null && childCount > 1) {endView = getChildAt(1)}return endView}private fun splitViewPositions(startView: View?, endView: View?): Array<Rect>? {if (windowLayoutInfo == null || startView == null || endView == null) {return null}// Calculate the area for view's content with paddingval paddedWidth = width - paddingLeft - paddingRightval paddedHeight = height - paddingTop - paddingBottomwindowLayoutInfo?.displayFeatures?.firstOrNull { feature -> isValidFoldFeature(feature) }?.let { feature ->getFeaturePositionInViewRect(feature, this)?.let {if (feature.bounds.left == 0) { // Horizontal layoutval topRect = Rect(paddingLeft, paddingTop,paddingLeft + paddedWidth, it.top)val bottomRect = Rect(paddingLeft, it.bottom,paddingLeft + paddedWidth, paddingTop + paddedHeight)if (measureAndCheckMinSize(topRect, startView) &&measureAndCheckMinSize(bottomRect, endView)) {return arrayOf(topRect, bottomRect)}} else if (feature.bounds.top == 0) { // Vertical layoutval leftRect = Rect(paddingLeft, paddingTop,it.left, paddingTop + paddedHeight)val rightRect = Rect(it.right, paddingTop,paddingLeft + paddedWidth, paddingTop + paddedHeight)if (measureAndCheckMinSize(leftRect, startView) &&measureAndCheckMinSize(rightRect, endView)) {return arrayOf(leftRect, rightRect)}}}}// We have tried to fit the children and measured them previously. Since they didn't fit,// we need to measure again to update the stored values.measure(lastWidthMeasureSpec, lastHeightMeasureSpec)return null}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)lastWidthMeasureSpec = widthMeasureSpeclastHeightMeasureSpec = heightMeasureSpec}private fun measureAndCheckMinSize(rect: Rect, childView: View): Boolean {val widthSpec = MeasureSpec.makeMeasureSpec(rect.width(), MeasureSpec.AT_MOST)val heightSpec = MeasureSpec.makeMeasureSpec(rect.height(), MeasureSpec.AT_MOST)childView.measure(widthSpec, heightSpec)return childView.measuredWidthAndState and MEASURED_STATE_TOO_SMALL == 0 &&childView.measuredHeightAndState and MEASURED_STATE_TOO_SMALL == 0}private fun isValidFoldFeature(displayFeature: DisplayFeature) =(displayFeature as? FoldingFeature)?.let { feature ->getFeaturePositionInViewRect(feature, this) != null} ?: falseprivate fun getFeaturePositionInViewRect(displayFeature: DisplayFeature,view: View,includePadding: Boolean = true): Rect? {// The the location of the view in window to be in the same coordinate space as the feature.val viewLocationInWindow = IntArray(2)view.getLocationInWindow(viewLocationInWindow)// Intersect the feature rectangle in window with view rectangle to clip the bounds.val viewRect = Rect(viewLocationInWindow[0], viewLocationInWindow[1],viewLocationInWindow[0] + view.width, viewLocationInWindow[1] + view.height)// Include padding if neededif (includePadding) {viewRect.left += view.paddingLeftviewRect.top += view.paddingTopviewRect.right -= view.paddingRightviewRect.bottom -= view.paddingBottom}val featureRectInView = Rect(displayFeature.bounds)val intersects = featureRectInView.intersect(viewRect)if ((featureRectInView.width() == 0 && featureRectInView.height() == 0) ||!intersects) {return null}// Offset the feature coordinates to view coordinate space start pointfeatureRectInView.offset(-viewLocationInWindow[0], -viewLocationInWindow[1])return featureRectInView}}

01

创建分屏的布局文件xml

要实现分屏的效果显示,需要创建两个不同的布局文件,像图中的宫格列表,还有按钮的布局分别在两个不同的xml中。

split_layout_start.xml(宫格列表)

<?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"android:id="@+id/startLayout"android:layout_width="match_parent"android:layout_height="match_parent"><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recycler_view"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"android:layout_width="match_parent"android:layout_height="wrap_content"android:padding="10dp" /></androidx.constraintlayout.widget.ConstraintLayout>

split_layout_end.xml(按钮布局)

<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:id="@+id/endLayout"android:layout_width="match_parent"android:layout_height="match_parent"android:padding="20dp"app:layoutDescription="@xml/split_layout_end_scene"><Buttonandroid:id="@+id/btncreate"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="生成数据"android:layout_marginBottom="30dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toStartOf="@+id/btnadd"app:layout_constraintStart_toStartOf="parent" /><Buttonandroid:id="@+id/btnadd"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="插入数据"android:layout_marginBottom="30dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent" /><Buttonandroid:id="@+id/btndel"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="删除数据"android:layout_marginBottom="30dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toEndOf="@id/btnadd" /></androidx.constraintlayout.motion.widget.MotionLayout>

02

创建新的Activity

创建好了我们的SplitLayout后,我们再创建一个FoldActivity。其中布局文件就要引用我们创建的SplitLayout,里面包括了刚才创建的宫格列表和按钮布局。

activity_fold.xml

<?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"android:orientation="vertical"tools:context=".ui.view.FoldActivity"><pers.vaccae.mvidemo.ui.view.SplitLayoutandroid:id="@+id/split_layout"android:layout_width="match_parent"android:layout_height="match_parent"app:startViewId="@id/startLayout"app:endViewId="@id/endLayout"android:padding="5dp"><includeandroid:id="@id/startLayout"layout="@layout/split_layout_start" /><includeandroid:id="@+id/endLayout"layout="@layout/split_layout_end" /></pers.vaccae.mvidemo.ui.view.SplitLayout></androidx.constraintlayout.widget.ConstraintLayout>

03

实现动画效果

效果图片中可以看到,我们实现位移动画的是按钮的布局,其实就是通过MotionLayout实现的。

其中app:layoutDescription="@xml/split_layout_end_scene"是动画属性,我们当布局改为MotionLayout时,会提示要缺少layoutDescription,使用ALT+ENTER会自动创建这个xml文件,位置在res.xml下

split_layout_end_scene.xml

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"><ConstraintSet android:id="@+id/start"><Constraint android:id="@+id/btncreate" /><Constraint android:id="@+id/btnadd" /><Constraint android:id="@+id/btndel" /></ConstraintSet><ConstraintSet android:id="@+id/end"><Constraint android:id="@id/btncreate"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginEnd="30dp"app:layout_constraintBottom_toTopOf="@+id/btnadd"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toTopOf="parent"/><Constraint android:id="@id/btnadd"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginEnd="30dp"app:layout_constraintBottom_toTopOf="@+id/btndel"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toBottomOf="@id/btncreate"/><Constraint android:id="@id/btndel"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginEnd="30dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toBottomOf="@id/btnadd"/></ConstraintSet><Transitionapp:constraintSetEnd="@id/end"app:constraintSetStart="@+id/start"app:duration="500"/>
</MotionScene>

MotionScene的子元素属性标签

<Transition> 包含运动的基本定义。

其中里面的app:constraintSetStart 和 app:constraintSetEnd 指的是运动的端点。这些端点在 MotionScene 后面的 <ConstraintSet> 元素中定义。

app:duration 指定完成运动所需的毫秒数 。

<ConstraintSet>子元素定义一个场景约束集,并在 <ConstraintSet> 元素中使用 <Constraint> 元素定义单个 View 的属性约束。

android:id:设置当前约束集的 id。这个 id 值可被 <Transition> 元素的 app:constraintSetStart 或者 app:constraintSetEnd 引用。

<Constraint> 元素用来定义单个 View 的属性约束。

它支持对 View 的所有 ConstraintLayout 属性定义约束,以及对 View 的下面这些标准属性定义约束。

由上面的布局文件中可以看到,在start中,我们三个按钮的布局不变,而在end中,三个按钮的布局改为垂直布局了。代码中调用方式直接就是通过motionLayout.transitionToEnd()motionLayout.transitionToStart()跳转即可

定义motionlayout

判断竖屏展开时调用transitionToEnd,合上状态时调用transitionStart

FoldActivity代码:

package pers.vaccae.mvidemo.ui.viewimport android.content.res.Configuration
import android.graphics.drawable.ClipDrawable.HORIZONTAL
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.*
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowInfoTrackerDecorator
import androidx.window.layout.WindowLayoutInfoimport kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.fold
import kotlinx.coroutines.launch
import pers.vaccae.mvidemo.R
import pers.vaccae.mvidemo.bean.CDrugs
import pers.vaccae.mvidemo.ui.adapter.DrugsAdapter
import pers.vaccae.mvidemo.ui.intent.ActionIntent
import pers.vaccae.mvidemo.ui.intent.ActionState
import pers.vaccae.mvidemo.ui.viewmodel.MainViewModelclass FoldActivity : AppCompatActivity() {private val TAG = "X Fold"private val recyclerView: RecyclerView by lazy { findViewById(R.id.recycler_view) }private val btncreate: Button by lazy { findViewById(R.id.btncreate) }private val btnadd: Button by lazy { findViewById(R.id.btnadd) }private val btndel: Button by lazy { findViewById(R.id.btndel) }private lateinit var mainViewModel: MainViewModelprivate lateinit var drugsAdapter: DrugsAdapter//adapter的位置private var adapterpos = -1private lateinit var windowInfoTracker :WindowInfoTrackerprivate lateinit var windowLayoutInfoFlow : Flow<WindowLayoutInfo>private val splitLayout: SplitLayout by lazy { findViewById(R.id.split_layout) }private val motionLayout :MotionLayout by lazy { findViewById(R.id.endLayout) }override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_fold)windowInfoTracker = WindowInfoTracker.getOrCreate(this@FoldActivity)windowLayoutInfoFlow = windowInfoTracker.windowLayoutInfo(this@FoldActivity)observeFold()mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)drugsAdapter = DrugsAdapter(R.layout.rcl_item, mainViewModel.listDrugs)drugsAdapter.setOnItemClickListener { baseQuickAdapter, view, i ->adapterpos = i}val gridLayoutManager = GridLayoutManager(this, 3)recyclerView.layoutManager = gridLayoutManagerrecyclerView.adapter = drugsAdapter//初始化ViewModel监听observeViewModel()btncreate.setOnClickListener {Log.i(TAG, "create")lifecycleScope.launch {mainViewModel.actionIntent.send(ActionIntent.LoadDrugs)}}btnadd.setOnClickListener {lifecycleScope.launch {mainViewModel.actionIntent.send(ActionIntent.InsDrugs)}}btndel.setOnClickListener {lifecycleScope.launch {Log.i("status", "$adapterpos")val item = try {drugsAdapter.getItem(adapterpos)} catch (e: Exception) {CDrugs()}mainViewModel.actionIntent.send(ActionIntent.DelDrugs(adapterpos, item))}}}private fun observeFold() {lifecycleScope.launch(Dispatchers.Main) {lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {windowLayoutInfoFlow.collect { layoutInfo ->Log.i(TAG, "size:${layoutInfo.displayFeatures.size}")splitLayout.updateWindowLayout(layoutInfo)// New posture informationval foldingFeature = layoutInfo.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull()foldingFeature?.let {Log.i(TAG, "state:${it.state}")}when {isTableTopPosture(foldingFeature) ->Log.i(TAG, "TableTopPosture")isBookPosture(foldingFeature) ->Log.i(TAG, "BookPosture")isSeparating(foldingFeature) ->// Dual-screen devicefoldingFeature?.let {if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) {Log.i(TAG, "Separating HORIZONTAL")} else {Log.i(TAG, "Separating VERTICAL")motionLayout.transitionToEnd()}}else -> {Log.i(TAG, "NormalMode")motionLayout.transitionToStart()}}}}}}fun isTableTopPosture(foldFeature: FoldingFeature?): Boolean {return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL}fun isBookPosture(foldFeature: FoldingFeature?): Boolean {return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&foldFeature.orientation == FoldingFeature.Orientation.VERTICAL}fun isSeparating(foldFeature: FoldingFeature?): Boolean {return foldFeature?.state == FoldingFeature.State.FLAT && foldFeature.isSeparating}override fun onConfigurationChanged(newConfig: Configuration) {super.onConfigurationChanged(newConfig)Log.i(TAG, "configurationchanged")}private fun observeViewModel() {lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {mainViewModel.state.collect {when (it) {is ActionState.Normal -> {btncreate.isEnabled = truebtnadd.isEnabled = truebtndel.isEnabled = true}is ActionState.Loading -> {btncreate.isEnabled = falsebtncreate.isEnabled = falsebtncreate.isEnabled = false}is ActionState.Drugs -> {drugsAdapter.setList(it.drugs)
//                            drugsAdapter.setNewInstance(it.drugs)}is ActionState.Error-> {Toast.makeText(this@FoldActivity, it.msg, Toast.LENGTH_SHORT).show()}is ActionState.Info ->{Toast.makeText(this@FoldActivity, it.msg, Toast.LENGTH_SHORT).show()}}}}}}
}

这样折叠屏展开的Demo就完成了。

源码地址

https://github.com/Vaccae/AndroidMVIDemo.git

点击阅读原文可以看到“码云”的地址

往期精彩回顾

Android折叠屏开发学习(二)---使用Jetpack WindowManager监听折叠屏开合状态

Android折叠屏开发学习(一)---通过传感器获取铰链角度

Android MVI架构初探

Android折叠屏开发学习(三)---使用MotionLayout实现折叠屏分屏效果相关推荐

  1. Android 音视频开发学习思路

    Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的.只能通过一点点的学习和积累把这块的知识串联积累起来. 初级入门篇: Android 音视频开发(一) ...

  2. 小米android n 分屏,官方确认!这些小米手机都支持分屏

    ­ 本以为分屏在MIUI9上会成为标配功能,但目前最新的消息显示,即便是升级了MIUI9,也只有部分机型能够支持分屏. ­ 日前,小米在MIUI官方论坛发布公告称,MIUI9的分屏功能只支持只支持基于 ...

  3. 分屏 取消_记录Android7.0以上手机开启分屏后活动的生命周期变化

    真正的大师,总是怀着一颗学徒的心. 大家好,我是小黑,一个还没秃头的程序员~~~ 如今,很多Android手机已经支持多屏任务了,也就是说你可以边追剧边回好友消息了,所以,今天的内容是记录Androi ...

  4. 笔记本卡屏或者死机怎么办+合理利用win10自动分屏

    笔记本卡屏或者死机怎么办 ctrl + alt + delete键 合理利用win10自动分屏 win10自带分屏,win键+方向键→或←,是左右分屏,win键+↑或↓,是上下分屏

  5. Android折叠屏开发学习(一)---通过传感器获取铰链角度

    学更好的别人, 做更好的自己. --<微卡智享> 本文长度为5289字,预计阅读8分钟 前言 Vivo在4月11号发布的X Fold折叠屏手机,也是抢了好几周好总算拿到手了,既然已经有了折 ...

  6. Android之NDK开发学习总结

    Android之NDK开发 http://www.cnblogs.com/devinzhang/archive/2012/02/29/2373729.html 一.NDK产生的背景 Android平台 ...

  7. Android高级终端开发学习笔记(《疯狂Android讲义》第11章-第17章)

    Android高级终端开发笔记 2021/6/19 下午 13:34开始 多媒体应用开发 Android支持的音频格式有:MP3 WAV 3GP等.支持的视频格式有MP4 3GP等. 多媒体数据既可以 ...

  8. 全网最全Android车载应用开发学习路线规划

    自2016 年后,市场上的移动端岗位开始大幅缩减,移动端程序员却与日俱增,逐渐达到饱和状态 目前人才市场的巨变,反应着汽车行业的大变局 人们的脑海中,对未来汽车形态的想象已经变了,抛弃了精密的齿轮和轰 ...

  9. Android 百度地图开发(三)--- 实现比例尺功能和替换自带的缩放组件

    转载请注明出处:http://blog.csdn.net/xiaanming/article/details/11821523 貌似有些天没有写博客了,前段时间在忙找工作的事,面试了几家公司,表示反响 ...

最新文章

  1. 谷歌发布TensorFlow Privacy​:大幅提升AI模型中的隐私保护
  2. 如何用Chrome自带的截屏功能截取超过一个屏幕的网页
  3. MySQL补充部分-SQL逻辑查询语句执行顺序
  4. [翻译]现代Linux系统上的栈溢出攻击【转】
  5. 蓝桥杯-长草-代码(BFS)
  6. python读取txt文件存储数组_python : 将txt文件中的数据读为numpy数组或列表
  7. 编程控制网卡启用停用vbs版
  8. 麻省理工18年春软件构造课程阅读01“静态检查”
  9. ext2文件系统源代码之ext2.h
  10. APK可视化修改工具:APK改之理(APK IDE)
  11. ClickHouse 创建数据库建表视图字典 SQL
  12. excel自动调整列宽_Knime数据分析入门- 06 自动调整Excel中列序
  13. 微信8.0状态背景视频合集
  14. 小三上位中的数学问题
  15. matlab link offset,基于MATLAB教学型机器人空间轨迹仿真
  16. 【PHP-网页内容抓取】抓取网页内容的两种常用方法
  17. html对象下边框呈三角形,html5 - CSS-三角形边框无法正确呈现IE8 - 堆栈内存溢出...
  18. 【瑞吉外卖】发送短信验证码功能实现
  19. 达芬奇科学特展《穿越·创新·达芬奇:超越时代的创新者》
  20. 如何安装nginx第三方模块--add-module

热门文章

  1. JavaScript 基本类型与基本类型包装对象
  2. [分享]云时代来了!给大家分享一个山寨版的air disk解决方案【简单,但不完美】
  3. autojump env: python: No such file or directory
  4. mysql添加外键出现1452错误_MySQL添加外键失败ERROR 1452的解决
  5. 中华唐氏小湾族2010年人口统计
  6. 1038 非诚勿扰第二期
  7. joplin同步到apache webdav
  8. Pycharm代码块的设置
  9. Excel取中间几个字符
  10. Elasticsearch之Template详解