(Original) Customize DialogFragment and solve its memory leak problem

Preface

In daily development, dialog is a common function. We often need to pop up some pop-up boxes to remind users.
Today we have defined a convenient dialog base class BaseSimpleDialogFragment, which
supports quickly displaying a dialog.
The main functions are:
initAnimation: Set entry and exit animations
getGravity: Set the dialog display position (on the screen, middle, bottom)
getCanceledOnTouchOutside: Click on the blank space to close
getWindowWidth
getWindowHeight
getPaddingLeft: Dynamically set the width, height and spacing.
Overall, it is relatively simple and easy to expand.
When creating a dialog, you only need to implement BaseSimpleDialogFragment,
for example so:

class MyDialog: BaseSimpleDialogFragment()  {
    
    

  override val layoutId: Int = R.layout.dialog_my_show

  companion object {
    
    
    @JvmStatic
    fun newInstance(): MyDialog {
    
    
      val dialog = MyDialog()
      dialog.arguments = Bundle().apply {
    
    
//      putParcelableArrayList(DATA, data)
      }
      return dialog
    }
  }


  override fun initData() {
    
    
//    data = arguments?.getParcelableArrayList(DATA) ?: return
  }

  override fun initView(view: View) {
    
    
    layoutView.findViewById<Button>(R.id.cancle).setOnClickListener {
    
    
      dismissAllowingStateLoss()
    }
  }
}

Just a few lines of code for display:

      MyDialog.newInstance().apply {
    
    
        //传递数据
      }.show(supportFragmentManager)

Source code

The source code is posted here first:

abstract class BaseSimpleDialogFragment : DialogFragment() {
    
    

  abstract val layoutId: Int

  protected open fun initView(view: View) {
    
    
    //sonar
  }

  protected open fun initData() {
    
    
    //sonar
  }

  protected open fun initListener() {
    
    
    //sonar
  }

  lateinit var layoutView: View

  protected lateinit var mContext: Context


  override fun onAttach(context: Context) {
    
    
    super.onAttach(context)
    mContext = context
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    
    
    super.onCreate(savedInstanceState)
    initAnimation()
  }


  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View? {
    
    
    layoutView = inflater.inflate(layoutId, container, false)
    return layoutView
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    
    
    super.onViewCreated(view, savedInstanceState)
    initView(view)
    initListener()
    initData()
  }


  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    
    
    val dialog = super.onCreateDialog(savedInstanceState).apply {
    
    
      window?.run {
    
    
        decorView.setPadding(
          getPaddingLeft(),
          getPaddingTop(),
          getPaddingRight(),
          getPaddingBottom()
        )
        val wlp = attributes.apply {
    
    
          gravity = getGravity()
          width = getWindowWidth()
          height = getWindowHeight()
        }
        attributes = wlp
        setWindowParam(this)
      }

      setCanceledOnTouchOutside(getCanceledOnTouchOutside())
    }

    isCancelable = getCancelable()
    return dialog
  }

  protected open fun setWindowParam(window: Window) {
    
    
    //sonar 
  }

  protected open fun initAnimation() {
    
    
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    
    
      setStyle(STYLE_NORMAL, R.style.FragmentDialogStyleWithAni)
    } else {
    
    
      setStyle(STYLE_NORMAL, R.style.FragmentDialogStyle_Low_Level_WithAni)
    }
  }

  protected open fun getGravity(): Int {
    
    
    return Gravity.CENTER
  }

  protected open fun getWindowWidth(): Int {
    
    
    return WindowManager.LayoutParams.MATCH_PARENT
  }

  protected open fun getWindowHeight(): Int {
    
    
    return WindowManager.LayoutParams.WRAP_CONTENT
  }

  protected open fun getPaddingLeft(): Int {
    
    
    return 0
  }

  protected open fun getPaddingRight(): Int {
    
    
    return 0
  }

  protected open fun getPaddingTop(): Int {
    
    
    return 0
  }

  protected open fun getPaddingBottom(): Int {
    
    
    return 0
  }

  protected open fun getCanceledOnTouchOutside(): Boolean {
    
    
    return false
  }

  protected open fun getCancelable(): Boolean {
    
    
    return true
  }

  override fun dismiss() {
    
    
    dismissAllowingStateLoss()
  }

  open fun show(manager: FragmentManager) {
    
    
    show(manager, javaClass.simpleName)
  }


  override fun show(manager: FragmentManager, tag: String?) {
    
    
    try {
    
    
      super.show(manager, tag)
    } catch (e: Exception) {
    
    
      Log.e("print", "show: $e")
    }
  }

}

Style used:


  <style name="FragmentDialogStyleWithAni" parent="FragmentDialogStyle">
    <item name="android:windowAnimationStyle">@style/DialogAnimation</item>
  </style>


  <style name="FragmentDialogStyle_Low_Level_WithAni" parent="FragmentDialogStyle_Low_Level">
    <item name="android:windowAnimationStyle">@style/DialogAnimation</item>
  </style>

  <style name="DialogAnimation" parent="@android:style/Animation.Dialog">
    <item name="android:windowEnterAnimation">@anim/push_ani_up_in</item>
    <item name="android:windowExitAnimation">@anim/push_ani_down_out</item>
  </style>

  <style name="FragmentDialogStyle_Low_Level" parent="android:Theme.Holo.Light.Dialog">
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowFrame">@null</item>
    <item name="android:backgroundDimEnabled">true</item>
    <item name="android:windowIsTranslucent">false</item>
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowContentOverlay">@null</item>
  </style>

  <style name="FragmentDialogStyle" parent="Base.AlertDialog.AppCompat.Light">
    <!--点击窗口外是否消失-->
    <item name="android:windowCloseOnTouchOutside">true</item>
    <!-- 背景颜色及透明程度 -->
    <item name="android:windowBackground">@android:color/transparent</item>

    <!-- 是否半透明 -->
    <item name="android:windowIsTranslucent">false</item>
    <!-- 是否没有标题 -->
    <item name="android:windowNoTitle">true</item>
    <!-- 是否浮现在activity之上 设置成false则match_parent可以全屏-->
    <item name="android:windowIsFloating">true</item>
  </style>

There are also two default entry and exit animations

<?xml version="1.0" encoding="UTF-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
  <translate
    android:duration="500"
    android:fromYDelta="0"
    android:toYDelta="100%p" />
</set>
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
  <translate
    android:duration="500"
    android:fromYDelta="100%p"
    android:toYDelta="0" />
</set>

memory leak

When using this custom dialog,
I found that when exiting the page,
LeakCanary will report a memory leak after the dialog dismiss
. It probably looks like this:
Insert image description here
The code is very simple, posted here:

class MainActivity : AppCompatActivity() {
    
    

  lateinit var mydialog: MyDialog
  
  @SuppressLint("MissingInflatedId")
  override fun onCreate(savedInstanceState: Bundle?) {
    
    
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    findViewById<Button>(R.id.showbtn).setOnClickListener {
    
    
      mydialog = MyDialog.newInstance().apply {
    
    
        //传递数据
      }
      mydialog.show(supportFragmentManager)
    }
  }
}

There is a button inside the dialog. Clicking it will close the dialog:

    layoutView.findViewById<Button>(R.id.cancle).setOnClickListener {
    
    
      dismissAllowingStateLoss()
    }

I don’t know if you can see the reason. Looking
at the LeakCanary log, I was told that the dialogFragment received the onDestroy callback.
That is to say, it is destroyed, then gc should recycle the fragment object.
However, the current interface still holds a reference to the object, causing a memory leak.

When we click on the source code of dismissAllowingStateLoss, we
Insert image description here
can see: dismissAllowingStateLoss should remove this fragment,
but if it is held by the activity, it cannot be recycled by the memory, resulting in a memory leak.

solve

The solution is also very simple, and several methods are provided:
1: Simple and crude, when dismissing, set the reference of the Activity to null.
First, our kotlin code must be changed:

  var mydialog: MyDialog?=null

Then when dismissing, set the reference to Activity to null.

    mydialog=null
    mydialog?.dismiss()

2: Create a one-time object for use, that is, a local variable, so that the current interface no longer holds the dialog object globally.

    findViewById<Button>(R.id.showbtn).setOnClickListener {
    
    
      var mydialog = MyDialog.newInstance().apply {
    
    
        //传递数据
      }
      mydialog.show(supportFragmentManager)
    }

3: Weak reference dialog, using the characteristics of weak references to ensure that memory can be recycled smoothly

class MainActivity : AppCompatActivity() {
    
    

  lateinit var mydialog: WeakReference<MyDialog>

  @SuppressLint("MissingInflatedId")
  override fun onCreate(savedInstanceState: Bundle?) {
    
    
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    findViewById<Button>(R.id.showbtn).setOnClickListener {
    
    
      mydialog = WeakReference(MyDialog.newInstance().apply {
    
    
        //传递数据
      })
      mydialog.get()?.show(supportFragmentManager)
    }

  }
}

Regarding customizing DialogFragment to solve the problem of memory leaks, these are generally the methods. Let’s
talk about some possible pitfalls of DialogFragment.

Step on the trap

Modify the background color of dialogFragment

Use this method to modify the background color in the onCreateView method

    //设置dialog背景色为透明色
    dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
    //设置dialog窗体颜色透明
    dialog?.window?.setDimAmount(0f)

Solve the problem of opening the soft keyboard and moving the dialogFragment up

<!--这个属性设置为false可以防止输入法把弹窗顶上去-->
 <item name="android:windowIsFloating">false</item>

When you open the dialogFragment, exit to the background or open a new page, and then return, the dialogFragment will re-execute the animation.

Cause of the problem: When the dialogFragment is invisible, the animation will be re-executed.
Solution:
1. Add transition attributes to the window in onCreateView. In this way, DialogFragment has a transition animation effect.
2. Cancel the transition animation in onStop, so that the DialogFragment no longer has the transition animation effect. At this time, jump to other pages and return to the current dialogfragment. Since the Dialogfragment animation is canceled, the entry animation will not be executed again.
3. Set the transition animation for DialogFragment again in onResume. Note that you need to use a handler delay here, because the Activity only adds its Window to the WindowManager after onResume is executed, and then calls the setview method of ViewRootImpl to start View drawing. If no delay is used, it is equivalent to At this time, the transition animation effect is set for DialogFragment. Then it makes no sense for us to cancel the animation in onStop in step 2. Because you return to DialogFragment from other pages and execute onResume before drawing the page. At this time, if you set the animation of DialogFragment directly in onResume, then DialogFragment actually has the transition animation attribute, and the entry animation will still be executed again. So a handler delay is used here to avoid this time difference. (Set the transition animation of DialogFragment after the rendering of DialogFragment is completed, so that it will not interfere with step 2). At this time, the DialogFragment has a transition animation, so when we end the DialogFragment, there will be an exit animation, which just makes up for point 1.
The modified code is roughly as follows:


 private val orientation: Int = R.style.BottomAnimBottom //弹出的动画

 override fun onStop() {
    
    
        super.onStop()
        if (dialog != null && dialog?.window != null) {
    
    
            dialog?.window?.setWindowAnimations(0)
        }
    }
private val handler: Handler = Handler()
    override fun onResume() {
    
    
        super.onResume()
        handler.postDelayed({
    
    
            if (dialog != null && dialog?.window != null) {
    
    
                dialog?.window?.setWindowAnimations(orientation)
            }
        },500)
    }

    override fun onDestroy() {
    
    
        super.onDestroy()
        handler.removeCallbacksAndMessages(null)
    }
 
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    
    
   
        if (dialog != null && dialog?.window != null) {
    
    
            dialog?.window?.setWindowAnimations(orientation)
        }
        return view
    }

Our example above uses the setStyle method to set the animation effect in Style.
The windowAnimationStyle property is set to DialogAnimation,
so the setWindowAnimations method is not called in onCreateView
. However, the modifications in other places are the same. Here is a modified code:

abstract class BaseSimpleAnimDialogFragment : BaseSimpleDialogFragment() {
    
    

  private var dialogAnimation: Int = R.style.DialogAnimation //弹出的动画

  private val handler: Handler = Handler()

  /**
   * 如果重写了initAnimation方法,也需要重写这个方法去设置入场动画
   * initAnimation:用来设置弹框样式,包括了进入动画
   * getdialogAnimation:只用来设置进入动画
   *
   * 所以如果设置的新样式里,默认动画已经改了,那么也要重写getdialogAnimation方法去修改动画
   * 这样才能确保onResume方法执行时,设置的动画和样式里的动画一致
   */
  protected open fun getdialogAnimation():Int {
    
    
      return R.style.DialogAnimation
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    
    
    super.onCreate(savedInstanceState)
    dialogAnimation = getdialogAnimation()
  }


  override fun onResume() {
    
    
    super.onResume()
    handler.postDelayed({
    
    
      if (dialog != null && dialog?.window != null) {
    
    
        dialog?.window?.setWindowAnimations(dialogAnimation)
      }
    },500)
  }

  override fun onStop() {
    
    
    super.onStop()
    if (dialog != null && dialog?.window != null) {
    
    
      dialog?.window?.setWindowAnimations(0)
    }
  }
  override fun onDestroy() {
    
    
    super.onDestroy()
    handler.removeCallbacksAndMessages(null)
  }

}

Subclasses need to override the initAnimation and getdialogAnimation methods to modify the animation.
Make sure that the windowAnimationStyle attribute of the Style set in the initAnimation method is consistent with the attribute Style returned by getdialogAnimation.

Things to note when restoring Fragment: InstantiationException

You can refer to this blog for this question:
Things to note when restoring Fragment: InstantiationException, do not write a constructor method with parameters in Fragment.
The main reason is that there are parameters in the constructor method of Fragment that need to be passed. Extract
the behavior of passing parameters into a method and give it to the outside world. Just call

Guess you like

Origin blog.csdn.net/Android_xiong_st/article/details/131946903